'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 / WOOCOMMERCESCRATCH.
Turn the moment after checkout into a reason to come back.
4A.
SCRATCHED
PRIZE 01
PRIZE 02
NO PRIZE
04Test 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.
4A / SCRATCH / TEST
Use your finger or mouse to scratch. This is test mode.
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();