'yes', 'title' => 'A little surprise for you', 'subtitle' => 'Scratch the card to reveal your reward.', 'surface_text' => 'SCRATCH HERE', 'trigger_status' => 'processing', 'coupon_expiry_days' => 30, 'prize_1_label' => '10% off your next order', 'prize_1_type' => 'percent', 'prize_1_amount' => 10, 'prize_1_weight' => 20, 'prize_2_label' => '€5 off your next order', 'prize_2_type' => 'fixed_cart', 'prize_2_amount' => 5, 'prize_2_weight' => 10, 'no_prize_title' => 'NO LUCK THIS TIME', 'no_prize_label' => 'No prize this time. Better luck next time!', 'no_prize_weight' => 70, ) ); } } ); add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), static function ( array $links ): array { if ( class_exists( 'WooCommerce' ) ) { array_unshift( $links, '' . esc_html__( 'Settings', '4a-scratch' ) . '' ); } if ( ! foura_scratch_pro_is_active() ) { $links[] = '' . esc_html__( 'Upgrade to Pro', '4a-scratch' ) . ''; } return $links; } ); final class FourA_Scratch { private static ?self $instance = null; private array $settings = array(); public static function instance(): self { return self::$instance ??= new self(); } private function __construct() { add_action( 'plugins_loaded', array( $this, 'boot' ) ); } public function boot(): void { if ( foura_scratch_pro_is_active() ) return; if ( ! class_exists( 'WooCommerce' ) ) { add_action( 'admin_notices', array( $this, 'wc_notice' ) ); return; } $this->settings = wp_parse_args( get_option( 'foura_scratch_settings', array() ), $this->defaults() ); add_action( 'admin_menu', array( $this, 'admin_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_assets' ) ); add_action( 'admin_post_foura_scratch_save', array( $this, 'save_settings' ) ); add_action( 'wp_ajax_foura_scratch_test_reveal', array( $this, 'ajax_test_reveal' ) ); add_action( 'woocommerce_thankyou', array( $this, 'thankyou_card' ), 24 ); add_action( 'woocommerce_order_details_after_order_table', array( $this, 'account_card' ), 20 ); add_action( 'wp_enqueue_scripts', array( $this, 'front_assets' ) ); add_action( 'wp_ajax_foura_scratch_reveal', array( $this, 'ajax_reveal' ) ); add_action( 'wp_ajax_nopriv_foura_scratch_reveal', array( $this, 'ajax_reveal' ) ); } private function defaults(): array { return array( 'enabled'=>'yes','title'=>'A little surprise for you','subtitle'=>'Scratch the card to reveal your reward.','surface_text'=>'SCRATCH HERE','trigger_status'=>'processing','coupon_expiry_days'=>30, 'prize_1_label'=>'10% off your next order','prize_1_type'=>'percent','prize_1_amount'=>10,'prize_1_weight'=>20, 'prize_2_label'=>'€5 off your next order','prize_2_type'=>'fixed_cart','prize_2_amount'=>5,'prize_2_weight'=>10, 'no_prize_title'=>'NO LUCK THIS TIME','no_prize_label'=>'No prize this time. Better luck next time!','no_prize_weight'=>70, ); } public function wc_notice(): void { if ( current_user_can( 'activate_plugins' ) ) echo '

4A Scratch requires WooCommerce.

'; } public function admin_menu(): void { add_menu_page( '4A Scratch', '4A Scratch', 'manage_woocommerce', '4a-scratch', array( $this, 'settings_page' ), $this->menu_icon(), 56 ); } private function menu_icon(): string { $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); } public function admin_assets( string $hook ): void { if ( 'toplevel_page_4a-scratch' !== $hook ) return; wp_enqueue_style( '4a-scratch-admin', FOURA_SCRATCH_URL . 'assets/admin.css', array(), FOURA_SCRATCH_VERSION ); wp_enqueue_script( '4a-scratch-admin', FOURA_SCRATCH_URL . 'assets/admin.js', array(), FOURA_SCRATCH_VERSION, true ); wp_enqueue_style( '4a-scratch', FOURA_SCRATCH_URL . 'assets/scratch.css', array(), FOURA_SCRATCH_VERSION ); wp_enqueue_script( '4a-scratch', FOURA_SCRATCH_URL . 'assets/scratch.js', array(), FOURA_SCRATCH_VERSION, true ); wp_localize_script( '4a-scratch', 'FourAScratch', array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'foura_scratch_reveal' ), 'testNonce' => wp_create_nonce( 'foura_scratch_test_reveal' ), 'error' => __( 'Something went wrong. Please try again.', '4a-scratch' ), ) ); } public function front_assets(): void { if ( ! is_order_received_page() && ! is_account_page() ) return; wp_enqueue_style( '4a-scratch', FOURA_SCRATCH_URL . 'assets/scratch.css', array(), FOURA_SCRATCH_VERSION ); wp_enqueue_script( '4a-scratch', FOURA_SCRATCH_URL . 'assets/scratch.js', array(), FOURA_SCRATCH_VERSION, true ); wp_localize_script( '4a-scratch', 'FourAScratch', array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'foura_scratch_reveal' ), 'error' => __( 'Something went wrong. Please try again.', '4a-scratch' ), ) ); } private function eligible( WC_Order $order ): bool { if ( 'yes' !== $this->settings['enabled'] ) return false; $status = (string) $this->settings['trigger_status']; if ( 'any_paid' === $status ) return $order->is_paid(); return $order->has_status( array( $status, 'completed' ) ); } public function thankyou_card( int $order_id ): void { $order = wc_get_order( $order_id ); if ( $order ) $this->render_card( $order ); } public function account_card( WC_Order $order ): void { $this->render_card( $order ); } private function render_card( WC_Order $order ): void { if ( ! $this->eligible( $order ) ) return; $result = $order->get_meta( '_foura_scratch_result', true ); $coupon = $order->get_meta( '_foura_scratch_coupon', true ); $key = $order->get_order_key(); ?>
4A / SCRATCH

settings['title'] ); ?>

settings['subtitle'] ); ?>

settings['no_prize_title'] ) : esc_html__( 'YOU WON', '4a-scratch' ); ?> get_meta( '_foura_scratch_label', true ) ); ?>
••••••
settings['surface_text'] ); ?>4A.

get_order_key(), $order_key ) || ! $this->eligible( $order ) ) { wp_send_json_error( array( 'message' => __( 'This scratch card is not available.', '4a-scratch' ) ), 403 ); } if ( is_user_logged_in() && $order->get_user_id() && get_current_user_id() !== $order->get_user_id() && ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'This scratch card does not belong to your account.', '4a-scratch' ) ), 403 ); } $existing = (string) $order->get_meta( '_foura_scratch_result', true ); if ( $existing ) { wp_send_json_success( $this->response_for_order( $order ) ); } $prize = $this->draw_prize(); $coupon = ''; if ( 'none' !== $prize['key'] ) $coupon = $this->create_coupon( $order, $prize ); $order->update_meta_data( '_foura_scratch_result', $prize['key'] ); $order->update_meta_data( '_foura_scratch_label', $prize['label'] ); $order->update_meta_data( '_foura_scratch_coupon', $coupon ); $order->update_meta_data( '_foura_scratch_revealed_at', current_time( 'mysql' ) ); $order->save(); $this->increment_stats( $prize['key'] ); wp_send_json_success( $this->response_for_order( $order ) ); } public function ajax_test_reveal(): void { check_ajax_referer( 'foura_scratch_test_reveal', 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', '4a-scratch' ) ), 403 ); } $force = sanitize_key( wp_unslash( $_POST['force'] ?? 'random' ) ); if ( ! in_array( $force, array( 'random', 'prize_1', 'prize_2', 'none' ), true ) ) $force = 'random'; $prize = 'random' === $force ? $this->draw_prize() : $this->get_prize( $force ); // Test mode deliberately creates no WooCommerce coupon and changes no statistics/order data. $coupon = 'none' === $prize['key'] ? '' : 'TEST-' . strtoupper( wp_generate_password( 6, false, false ) ); wp_send_json_success( array( 'won' => 'none' !== $prize['key'], 'title' => 'none' === $prize['key'] ? (string) $this->settings['no_prize_title'] : 'YOU WON', 'label' => $prize['label'], 'coupon' => $coupon, 'test' => true, ) ); } private function get_prize( string $key ): array { $items = array( 'prize_1' => array( 'key'=>'prize_1','label'=>(string)$this->settings['prize_1_label'],'type'=>(string)$this->settings['prize_1_type'],'amount'=>(float)$this->settings['prize_1_amount'],'weight'=>max(0,(int)$this->settings['prize_1_weight'])), 'prize_2' => array( 'key'=>'prize_2','label'=>(string)$this->settings['prize_2_label'],'type'=>(string)$this->settings['prize_2_type'],'amount'=>(float)$this->settings['prize_2_amount'],'weight'=>max(0,(int)$this->settings['prize_2_weight'])), 'none' => array( 'key'=>'none','label'=>(string)$this->settings['no_prize_label'],'type'=>'none','amount'=>0,'weight'=>max(0,(int)$this->settings['no_prize_weight'])), ); return $items[ $key ] ?? $items['none']; } private function draw_prize(): array { $items = array( array( 'key'=>'prize_1','label'=>(string)$this->settings['prize_1_label'],'type'=>(string)$this->settings['prize_1_type'],'amount'=>(float)$this->settings['prize_1_amount'],'weight'=>max(0,(int)$this->settings['prize_1_weight'])), array( 'key'=>'prize_2','label'=>(string)$this->settings['prize_2_label'],'type'=>(string)$this->settings['prize_2_type'],'amount'=>(float)$this->settings['prize_2_amount'],'weight'=>max(0,(int)$this->settings['prize_2_weight'])), array( 'key'=>'none','label'=>(string)$this->settings['no_prize_label'],'type'=>'none','amount'=>0,'weight'=>max(0,(int)$this->settings['no_prize_weight'])), ); $total = array_sum( array_column( $items, 'weight' ) ); if ( $total <= 0 ) return $items[2]; $roll = random_int( 1, $total ); $cursor = 0; foreach ( $items as $item ) { $cursor += $item['weight']; if ( $roll <= $cursor ) return $item; } return $items[2]; } private function create_coupon( WC_Order $order, array $prize ): string { $code = '4A-' . strtoupper( wp_generate_password( 8, false, false ) ); $coupon = new WC_Coupon(); $coupon->set_code( $code ); $coupon->set_discount_type( in_array( $prize['type'], array( 'percent','fixed_cart' ), true ) ? $prize['type'] : 'fixed_cart' ); $coupon->set_amount( (float) $prize['amount'] ); $coupon->set_usage_limit( 1 ); $coupon->set_individual_use( true ); $email = $order->get_billing_email(); if ( $email ) $coupon->set_email_restrictions( array( $email ) ); $days = max( 0, (int) $this->settings['coupon_expiry_days'] ); if ( $days ) $coupon->set_date_expires( time() + DAY_IN_SECONDS * $days ); $coupon->set_description( sprintf( '4A Scratch reward from order #%s', $order->get_order_number() ) ); $coupon->save(); return $code; } private function response_for_order( WC_Order $order ): array { $result = (string) $order->get_meta( '_foura_scratch_result', true ); return array( 'won' => 'none' !== $result, 'title' => 'none' === $result ? (string) $this->settings['no_prize_title'] : 'YOU WON', 'label' => (string) $order->get_meta( '_foura_scratch_label', true ), 'coupon' => (string) $order->get_meta( '_foura_scratch_coupon', true ), ); } private function increment_stats( string $key ): void { $stats = wp_parse_args( get_option( 'foura_scratch_stats', array() ), array( 'revealed'=>0,'prize_1'=>0,'prize_2'=>0,'none'=>0 ) ); $stats['revealed']++; if ( isset( $stats[$key] ) ) $stats[$key]++; update_option( 'foura_scratch_stats', $stats, false ); } public function settings_page(): void { if ( ! current_user_can( 'manage_woocommerce' ) ) return; $s = wp_parse_args( get_option( 'foura_scratch_settings', array() ), $this->defaults() ); $stats = wp_parse_args( get_option( 'foura_scratch_stats', array() ), array( 'revealed'=>0,'prize_1'=>0,'prize_2'=>0,'none'=>0 ) ); $total_weight = max( 1, (int)$s['prize_1_weight'] + (int)$s['prize_2_weight'] + (int)$s['no_prize_weight'] ); ?>
4A WORKS / WOOCOMMERCE

SCRATCH.

Turn the moment after checkout into a reason to come back.

4A.

SCRATCHED
PRIZE 01
PRIZE 02
NO PRIZE
01

Campaign

The essentials for your scratch card.

02

Rewards

Weights are relative chances. Example: 20 / 10 / 70 = 20% / 10% / 70%.

reward_fields(1,$s,$total_weight); $this->reward_fields(2,$s,$total_weight); ?>
NO PRIZE%

0 = no expiry. Winning coupons can be used once and are restricted to the order email address.

03

Pro controls PRO

Already visible, ready when your store needs more.

Multiple campaignsDifferent cards for seasons, products or customer groups.
Free product rewardsWin a product instead of only a coupon.
SchedulingStart and stop campaigns automatically.
Advanced conditionsOrder value, category, product and customer rules.
Design controlsBrand colors, card styles and custom artwork.
Advanced analyticsRedemption and campaign performance insights.
DISCOVER 4A SCRATCH PRO →
Ready to go?Save your settings. Changes apply immediately.
04

Test mode

Test the exact scratch experience without placing an order.

Safe test: no real coupon is created, no order is changed and test scratches do not count in your statistics.

PRIZE 0%
isset($_POST['enabled'])?'yes':'no', 'title'=>sanitize_text_field(wp_unslash($_POST['title']??'')), 'subtitle'=>sanitize_text_field(wp_unslash($_POST['subtitle']??'')), 'surface_text'=>sanitize_text_field(wp_unslash($_POST['surface_text']??'')), 'trigger_status'=>$trigger, 'coupon_expiry_days'=>max(0,absint( wp_unslash( $_POST['coupon_expiry_days'] ?? '30' ) )), 'prize_1_label'=>sanitize_text_field(wp_unslash($_POST['prize_1_label']??'')), 'prize_1_type'=>$type1, 'prize_1_amount'=>max(0,floatval( sanitize_text_field( wp_unslash( $_POST['prize_1_amount'] ?? '0' ) ) )), 'prize_1_weight'=>max(0,absint( wp_unslash( $_POST['prize_1_weight'] ?? '0' ) )), 'prize_2_label'=>sanitize_text_field(wp_unslash($_POST['prize_2_label']??'')), 'prize_2_type'=>$type2, 'prize_2_amount'=>max(0,floatval( sanitize_text_field( wp_unslash( $_POST['prize_2_amount'] ?? '0' ) ) )), 'prize_2_weight'=>max(0,absint( wp_unslash( $_POST['prize_2_weight'] ?? '0' ) )), 'no_prize_title'=>sanitize_text_field(wp_unslash($_POST['no_prize_title']??'HELAAS…')), 'no_prize_label'=>sanitize_text_field(wp_unslash($_POST['no_prize_label']??'')), 'no_prize_weight'=>max(0,absint( wp_unslash( $_POST['no_prize_weight'] ?? '0' ) )), ); if ( ($settings['prize_1_weight']+$settings['prize_2_weight']+$settings['no_prize_weight']) <= 0 ) $settings['no_prize_weight']=100; update_option( 'foura_scratch_settings', $settings ); set_transient( 'foura_scratch_saved_' . get_current_user_id(), 1, 60 ); wp_safe_redirect( add_query_arg( array( 'page' => '4a-scratch' ), admin_url( 'admin.php' ) ) ); exit; } } FourA_Scratch::instance();