Settings -> General Settings (option key: * fluent_cart_checkout_form_subscribe_settings, defined in the base plugin's * AutoSubscribe service). */ class CheckoutSubscription { /** * Form field name posted with the checkout request and the * order meta key holding the captured opt-in state. */ const OPTIN_FIELD = '_fc_cart_checkout_subscribe'; /** * Order meta flag preventing the same order from being processed twice. */ const PROCESSED_META = '_fc_cart_checkout_optin_processed'; public function init() { // Renders inside the checkout
on both the standard checkout // page and the modal checkout (FormData picks the value up on submit) add_action('fluent_cart/before_payment_methods', [$this, 'renderOptinCheckbox']); // Fires while the order is being created from the checkout request — // the only point where the raw POST data is available add_action('fluent_cart/checkout/prepare_other_data', [$this, 'captureOptinState']); // Async (Action Scheduler) hook recommended by FluentCart for // third-party post-payment processing add_action('fluent_cart/order_paid_done', [$this, 'maybeSubscribeContact'], 20); } protected function getSettings() { return (new AutoSubscribe())->getFluentCartCheckoutSettings(); } /** * Prints the opt-in checkbox above the payment methods section, * using FluentCart's native checkbox markup classes. */ public function renderOptinCheckbox($data) { $settings = $this->getSettings(); if (Arr::get($settings, 'status') != 'yes') { return; } if (Arr::get($settings, 'show_only_new') == 'yes') { $contact = fluentcrm_get_current_contact(); if ($contact && $contact->status == 'subscribed') { return; } } $label = Arr::get($settings, 'checkbox_label'); if (!$label) { $label = __('Sign me up for the newsletter!', 'fluent-crm'); } $isChecked = Arr::get($settings, 'auto_checked') == 'yes'; ?>
getSettings(); if (Arr::get($settings, 'status') != 'yes') { return; } $isChecked = Arr::get((array)Arr::get($eventData, 'request_data', []), self::OPTIN_FIELD) == '1'; $order->updateMeta(self::OPTIN_FIELD, $isChecked ? '1' : '0'); } /** * Creates/updates the FluentCRM contact when a paid order has the * opt-in flag. Runs async via fluent_cart/order_paid_done. Idempotent — * payment retries or hook re-runs will not duplicate processing. */ public function maybeSubscribeContact($eventData) { $order = Arr::get($eventData, 'order'); $customer = Arr::get($eventData, 'customer'); if (!$order || !$customer) { return false; } // Treat both the final marker and an in-flight claim as processed so // serial retries of the paid-order event (e.g. Action Scheduler // re-runs) cannot double-process. This read-then-write guard is not // atomic, so it does not protect against truly concurrent invocations $processedState = $order->getMeta(self::PROCESSED_META); if ($processedState == 'yes' || $processedState == 'processing') { return false; } if ($order->getMeta(self::OPTIN_FIELD) != '1') { return false; } $settings = $this->getSettings(); if (Arr::get($settings, 'status') != 'yes') { return false; } $email = sanitize_email($customer->email); if (!$email || !is_email($email)) { return false; } $orderBillingAddress = $order->billing_address; $address1 = Arr::get($orderBillingAddress, 'address_1', ''); $address2 = Arr::get($orderBillingAddress, 'address_2', ''); $state = AddressHelper::getStateNameByCode($customer->state, $customer->country); $subscriberData = [ 'first_name' => sanitize_text_field($customer->first_name), 'last_name' => sanitize_text_field($customer->last_name), 'country' => sanitize_text_field($customer->country), 'state' => sanitize_text_field($state), 'city' => sanitize_text_field($customer->city), 'postal_code' => sanitize_text_field($customer->postcode), 'address_line_1' => sanitize_text_field($address1), 'address_line_2' => sanitize_text_field($address2), 'email' => $email ]; if ($listId = Arr::get($settings, 'target_list')) { $subscriberData['lists'] = [$listId]; } if ($tags = Arr::get($settings, 'target_tags')) { $subscriberData['tags'] = $tags; } if (Arr::get($settings, 'double_optin') == 'yes') { $subscriberData['status'] = 'pending'; } else { $subscriberData['status'] = 'subscribed'; } $subscriberData = apply_filters('fluent_crm/fluent_cart_checkout_auto_subscribe_data', $subscriberData, $order); // Claim the order before contact creation / email side effects so an // Action Scheduler retry re-entering this handler cannot run them twice $order->updateMeta(self::PROCESSED_META, 'processing'); try { $contact = FunnelHelper::createOrUpdateContact($subscriberData); if (!$contact) { // Release the claim so a later retry can attempt processing again $order->updateMeta(self::PROCESSED_META, '0'); return false; } if ($contact->status == 'pending') { $contact->sendDoubleOptinEmail(); } } catch (\Throwable $e) { // Release the claim on failure too — otherwise the order would be // stuck in 'processing' and permanently skipped by the guard above $order->updateMeta(self::PROCESSED_META, '0'); throw $e; } $order->updateMeta(self::PROCESSED_META, 'yes'); return true; } }