SubscriptionFlow + Wix Integration — Product Setup Guide
Platform: Wix • Audience: Store owners and developers • Purpose: Configure products to support both one-time and subscription purchases using a variant-based approach.
1. Setting Up Subscription Products in Wix
Wix does not natively support SubscriptionFlow product types. Instead, subscription functionality is implemented using a variant-based structure. This allows a single product to support both one-time and recurring purchase options.
Each purchase type (e.g., One-Time, Monthly) is configured as a product variant. These variants are later used to determine checkout behavior and pricing.
Figure 1: Example of product with subscription variants
1.1 Accessing Product Settings
- Go to Wix Dashboard
- Navigate to Store Products
- Create a new product or edit an existing one
- Scroll to the Product Options section
1.2 Creating Purchase Option
Add a new product option that will control purchase type selection:
- Option Name: Purchase Option
- Field Type: Text
This option allows customers to choose between one-time and subscription purchases directly on the product page.
1.3 Defining Subscription Variants
Add the following values:
- One-Time
- Every 2 Weeks
- Every Month
- Every 2 Months
Each value represents a different purchase type and will be used later to determine checkout behavior.
1.4 Generating Variants
- Click Apply after adding options
- Wix will automatically generate product variants
Each variant now represents a selectable purchase option for the customer.
1.5 Pricing Configuration
Configure pricing for each variant:
| Purchase Option | Pricing Strategy |
|---|---|
| One-Time | Regular price |
| Every 2 Weeks | Discounted price |
| Every Month | Discounted price |
| Every 2 Months | Discounted price |
This pricing model enables “Subscribe & Save” functionality.
1.6 SKU Configuration
Assign unique SKUs for tracking:
| Variant | Example SKU |
|---|---|
| One-Time | ONE-TIME |
| Every 2 Weeks | 2-WEEK |
| Every Month | 1-MONTH |
| Every 2 Months | 2-MONTH |
1.7 Important Naming Guidelines
- Use "One-Time" for non-subscription purchases
- Use "Every" in all subscription options
Avoid:
- Monthly
- Weekly
- Regular
- Custom labels without “Every”
1.8 How This Setup Works
Once configured, the system reads the selected variant on the frontend:
- One-Time → Wix Checkout
- Subscription → SubscriptionFlow Checkout
This allows seamless switching between checkout flows without needing separate products.
1.9 Final Outcome
After completing this step:
- Products support both one-time and subscription purchases
- Variants are properly structured for detection
- Pricing is configured for subscription discounts
- System is ready for frontend and checkout integration
SubscriptionFlow + Wix Integration — Backend Setup Guide
Platform: Wix • Audience: Developers • Purpose: Configure backend APIs to securely connect Wix with SubscriptionFlow.
2. Backend Setup (API Integration)
This step establishes a secure connection between your Wix website and SubscriptionFlow using backend functions. It enables authentication, customer data retrieval, and cart data access.
2.1 Creating Backend File
- Open Wix Editor
- Enable Dev Mode
- Navigate to Backend
- Create a new file:
siteApi.web.js
This file will act as a secure bridge between frontend code and SubscriptionFlow APIs.
2.2 API Configuration
Add your SubscriptionFlow credentials inside the backend file:
const CLIENT_ID = "YOUR_CLIENT_ID";
const CLIENT_SECRET = "YOUR_CLIENT_SECRET";
const Base_URL = "https://yourdomain.subscriptionflow.com";
const TOKEN_URL = `${Base_URL}/oauth/token`;
2.3 Authentication (OAuth)
SubscriptionFlow uses OAuth authentication. The backend must request an access token before making API calls.
The token is:
- Requested using client credentials
- Stored temporarily
- Reused until expiration
This improves performance and avoids unnecessary API requests.
2.4 Fetching Customer Data
A backend function is created to retrieve customer details from SubscriptionFlow using email.
This is used for:
- Prefilling checkout information
- Identifying returning customers
- Syncing user data between systems
The function accepts an email and returns customer data if found.
2.5 Fetching Cart Data
Another backend function retrieves the current Wix cart.
This includes:
- Cart items
- Product IDs
- Variant details
- Quantity
- Pricing
This data is later used to determine whether the cart contains subscription products.
2.6 Frontend Usage
The frontend (product and cart pages) calls backend functions to retrieve data:
- Customer data → for checkout prefill
- Cart data → for checkout logic
This ensures sensitive operations remain secure while still being accessible where needed.
2.7 Data Flow
Frontend (Wix Pages)
↓
Backend (siteApi.web.js)
↓
SubscriptionFlow API
↓
Response (Customer / Token / Cart)
2.8 Security Considerations
Recommended approach:
- Store credentials in Wix Secrets Manager
- Access them securely in backend
- Never expose secrets in client-side code
2.9 Why This Step Matters
This backend layer is essential because it:
- Enables secure API communication
- Provides customer data for personalization
- Allows cart-based decision making
- Supports dynamic checkout routing
2.10 Final Outcome
After completing this step:
- Wix is connected to SubscriptionFlow
- Customer data can be fetched dynamically
- Cart data is accessible for logic handling
- System is ready for webhook integration (next step)
SubscriptionFlow + Wix Integration — Webhooks & Member Sync Guide
Platform: Wix • Audience: Developers • Purpose: Automatically sync SubscriptionFlow customers into Wix using webhooks.
3. Webhooks & Member Sync
Webhooks enable real-time communication between SubscriptionFlow and Wix. When specific events occur in SubscriptionFlow (such as subscription creation or payment), data is automatically sent to Wix.
This ensures that customers are created or updated in Wix without manual intervention.
3.1 Creating HTTP Functions File
- Go to Wix Editor
- Enable Dev Mode
- Navigate to Backend
- Create a new file:
http-functions.js
This file will handle incoming webhook requests from SubscriptionFlow.
3.2 What Are Webhooks?
Webhooks are automated HTTP requests sent from SubscriptionFlow to Wix when certain events occur.
Common events include:
- Subscription created
- Subscription payment completed
- Customer updated
3.3 Webhook Endpoints
The following endpoints should be created in Wix:
POST /_functions/updateOrCreateMember
POST /_functions/updateOrCreateMemberOnSubscriptionPaid
These endpoints will be configured in SubscriptionFlow to receive event data.
3.4 Webhook Flow
When a subscription event occurs:
- SubscriptionFlow sends webhook request to Wix
- Wix HTTP function receives the request
- Customer or subscription ID is extracted
- Backend fetches full data from SubscriptionFlow API
- Wix creates or updates the user
3.5 Fetching Subscription & Customer Data
After receiving a webhook, Wix backend must call SubscriptionFlow APIs to retrieve:
- Subscription details
- Customer information
- Billing and shipping data
This ensures accurate and complete data before creating or updating users.
3.6 Payment Validation
This prevents creating users for incomplete or failed transactions.
3.7 Creating or Updating Wix Members
After fetching customer data, the system checks if the user already exists in Wix:
- If user exists → Update data
- If user does not exist → Create new member
Data synced includes:
- Name
- Phone number
- Billing address
- Shipping address
3.8 Sending Member Access Email
When a new user is created:
- A password setup email is sent
- User can log into Wix member area
This enables access to subscription-related features such as account management.
3.9 Webhook URL Configuration
In SubscriptionFlow dashboard, configure webhook URLs as:
https://yourdomain.com/_functions/updateOrCreateMemberOnSubscriptionPaid
3.10 Data Flow
SubscriptionFlow
↓ (Webhook)
Wix HTTP Function
↓
Fetch Data via API
↓
Check User
↓
Create / Update Member
↓
Store in CRM
3.11 Why This Step Matters
This step ensures:
- Customer data stays synchronized between systems
- Users are automatically created in Wix
- Subscription management works correctly
- No manual data entry is required
3.12 Final Outcome
After completing this step:
- SubscriptionFlow events trigger automatic updates in Wix
- Customers are created instantly after subscription/payment
- CRM and member data remain consistent
- System is ready for frontend interaction (next steps)
SubscriptionFlow + Wix Integration — Custom Elements Guide
Platform: Wix • Audience: Developers • Purpose: Override Wix buttons and control checkout behavior using custom elements.
4. Custom Elements (Frontend Control Layer)
Custom elements are used to override Wix’s default Buy Now and Checkout buttons and enable dynamic redirection to SubscriptionFlow when subscription products are selected.
Since Wix does not allow full control over native checkout behavior, custom elements provide a flexible way to inject custom logic directly into the frontend.
4.1 Creating Custom Elements Folder
- Open Wix Editor
- Enable Dev Mode
- Navigate to Public folder
- Create a new folder:
custom-elements
This folder will contain all custom frontend components.
4.2 Product Page Custom Element
Create a file:
subscriptionflow-product-buy-now.js
This element controls the Buy Now button on the product page.
Behavior
- Detects Wix Buy Now button
- Hides it when subscription is selected
- Displays custom SubscriptionFlow button
- Restores Wix button for one-time purchases
4.3 Cart Page Custom Element
Create a file:
subscriptionflow-button-add.js
This element controls the Checkout button on the cart page.
Behavior
- Receives full cart data
- Checks if any item is a subscription
- Builds SubscriptionFlow checkout URL
- Replaces Wix checkout button when needed
4.4 Data Passed to Custom Elements
Custom elements rely on attributes passed from frontend code:
- customdatacart — full cart JSON
- customerdatjsons — customer data
- bunowbutton — SubscriptionFlow checkout URL
These attributes allow the element to make real-time decisions.
4.5 Subscription Detection Logic
Subscription items are detected based on the product option:
Purchase Option !== "One-Time"
Any value other than “One-Time” is treated as a subscription.
4.6 URL Generation
The custom element builds a SubscriptionFlow checkout URL dynamically.
It includes:
- Product ID
- Variant ID
- Quantity
- Customer data
- Coupon (if applied)
- Shipping method
4.7 Button Replacement Logic
When subscription is detected:
- Hide Wix default button
- Insert custom button
- Attach SubscriptionFlow URL
When no subscription is detected:
- Remove custom button
- Restore Wix default button
4.8 Advanced Features
The custom elements also support:
- Coupon detection and application
- Customer data prefill
- Shipping method handling
- Multi-item cart support
4.9 Data Flow
Frontend (Product / Cart Page)
↓
Send Data to Custom Element
↓
Custom Element Logic
↓
Replace Button
↓
Redirect to SubscriptionFlow
4.10 Why This Step Matters
This step is essential because:
- Wix does not allow direct checkout customization
- Default buttons cannot handle subscription logic
- SubscriptionFlow requires external checkout redirection
Custom elements solve all of these limitations.
4.11 Final Outcome
After completing this step:
- Buy Now button becomes dynamic
- Cart checkout is fully controlled
- Subscription and one-time flows are separated
- System is ready for page-level logic
SubscriptionFlow + Wix Integration — Product Page Logic Guide
Platform: Wix • Audience: Developers • Purpose: Detect subscription selections and dynamically control Buy Now behavior.
5. Product Page Logic
This step implements frontend logic on the product page to detect whether a user selects a subscription option or a one-time purchase, and dynamically adjust the checkout behavior.
5.1 Where to Add Code
- Open Wix Editor
- Go to Product Page
- Enable Dev Mode
- Add code in the page code panel
5.2 Core Responsibilities
This logic handles:
- Detecting selected product variant
- Identifying subscription vs one-time purchase
- Generating SubscriptionFlow checkout URL
- Passing data to custom element
5.3 Detecting Selected Variant
The system retrieves:
- Product ID
- Variant ID
- Selected options
- Quantity
This information is required to build the checkout URL.
5.4 Subscription Detection Logic
The system checks the value of:
Purchase Option
Detection rule:
If value contains "One-Time" → One-time purchase
Else → Subscription
5.5 Customer Data Handling
If the user is logged in:
- Fetch email from Wix Users
- Call backend to get customer data
- Store data in session
This data is later used to prefill SubscriptionFlow checkout fields.
5.6 Building SubscriptionFlow URL
When a subscription is selected, a dynamic URL is generated:
https://yourdomain.subscriptionflow.com/en/hosted-page/commerceflow
The URL includes:
- Product ID
- Variant ID
- Quantity
- Customer data
- Return cart URL
5.7 Passing Data to Custom Element
After generating the URL:
- If subscription → send URL to custom element
- If one-time → clear attribute
setAttribute('bunowbutton', fullUrl)
This triggers the custom element to update the Buy Now button.
5.8 Event Triggers
The logic must run when:
- Variant selection changes
- Quantity changes
This ensures real-time updates.
5.9 Behavior Summary
- Subscription selected: Custom button shown → redirects to SubscriptionFlow
- One-Time selected: Wix Buy Now restored
5.10 Data Flow
User selects variant
↓
Product Page Velo
↓
Check Purchase Option
↓
If Subscription → Build URL
↓
Send to Custom Element
↓
Update Button
5.11 Why This Step Matters
This step connects:
- Product setup (Step 1)
- Backend data (Step 2)
- Custom elements (Step 4)
It acts as the central decision layer for checkout behavior.
5.12 Final Outcome
After completing this step:
- Product page reacts dynamically to user selection
- SubscriptionFlow URLs are generated in real-time
- Buy Now button updates automatically
- System is ready for cart-level logic
SubscriptionFlow + Wix Integration — Cart Page Logic Guide
Platform: Wix • Audience: Developers • Purpose: Control checkout behavior based on cart contents and redirect to SubscriptionFlow when required.
6. Cart Page Logic
This step implements logic on the cart page to determine whether checkout should proceed through Wix or be redirected to SubscriptionFlow based on the presence of subscription products.
6.1 Where to Add Code
- Open Wix Editor
- Go to Cart Page
- Enable Dev Mode
- Add code in the page code panel
6.2 Core Responsibilities
This logic handles:
- Fetching cart data
- Fetching customer data
- Passing data to custom element
- Listening for cart updates
6.3 Fetching Customer Data
If the user is logged in:
- Retrieve user email
- Call backend function to fetch customer data
- Store data in session
This ensures customer information is available for checkout prefill.
6.4 Fetching Cart Data
Cart data is retrieved using a backend function:
myGetCurrentCartFunction()
The returned data includes:
- Cart items
- Product IDs
- Variant IDs
- Quantity
- Pricing and discounts
6.5 Passing Data to Custom Element
The following attributes are sent to the custom element:
- customdatacart — Full cart JSON
- customerdatjsons — Customer data
This enables the custom element to perform checkout logic.
6.6 Cart Change Detection
The logic listens for cart updates:
onCartChange
When the cart changes:
- Cart data is re-fetched
- Custom element is updated
This ensures real-time behavior.
6.7 Checkout Behavior
The custom element determines checkout behavior:
- Only One-Time items: Proceed with Wix checkout
- Any Subscription item: Redirect to SubscriptionFlow
6.8 Data Flow
Cart Page Load
↓
Fetch Cart Data (Backend)
↓
Fetch Customer Data (Session)
↓
Pass Data to Custom Element
↓
Detect Subscription
↓
Replace Checkout Button
↓
Redirect to SubscriptionFlow (if needed)
6.9 Role of Custom Element
The cart page itself does not:
- Detect subscription items directly
- Generate checkout URLs
Instead, it passes data to the custom element, which handles all logic.
6.10 Why This Step Matters
This step ensures:
- Correct checkout routing at cart level
- Support for mixed carts
- Real-time updates when cart changes
- Accurate coupon and pricing handling
6.11 Final Outcome
After completing this step:
- Cart dynamically reacts to contents
- Checkout button is fully controlled
- SubscriptionFlow integration works end-to-end
- System is fully functional
SubscriptionFlow + Wix Integration — Code Structure Reference
Platform: Wix • Audience: Developers • Purpose: Provide a structured layout for all required files. Add your implementation code in the sections below.
7. Code Structure Overview
The following structure outlines all required files for the integration. Insert your implementation code in the appropriate sections.
Backend/
├── siteApi.web.js
├── http-functions.js
Public/
├── custom-elements/
├── subscriptionflow-product-buy-now.js
├── subscriptionflow-button-add.js
Pages/
├── Product Page Code
├── Cart Page Code
7.1 Backend — siteApi.web.js
import { fetch } from 'wix-fetch';
import { webMethod, Permissions } from "wix-web-module";
import { currentUser } from 'wix-users-backend';
import { currentCart } from "wix-ecom-backend";
import wixData from 'wix-data';
const CLIENT_ID = "YOUR_CLIENT_ID";
const CLIENT_SECRET = "YOUR_CLIENT_SECRET";
const Base_URL="https://your-site.subscriptionflow.com";
const TOKEN_URL = `${Base_URL}/oauth/token`;
let accessToken = null;
let tokenExpiry = 0;
export const myGetCurrentCartFunction = webMethod(
Permissions.Anyone,
async () = {
try {
const myCurrentCart = await currentCart.getCurrentCart();
console.log("Success! Retrieved current cart:", currentCart);
return myCurrentCart;
} catch (error) {
console.error(error);
// Handle the error
}
},
);
// Function to get access token
async function getAccessToken() {
try {
const response = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}`
});
if (response.ok) {
const data = await response.json();
accessToken = data.access_token;
tokenExpiry = Date.now() + (data.expires_in * 1000);
return accessToken;
} else {
return null;
}
} catch (error) {
return null;
}
}
// ✅ Expose function to frontend (fetch customer details by email)
export const fetchCustomerDetail = webMethod(Permissions.Anyone, async (email) = {
try {
const token = await getAccessToken();
const requestUrl = `${Base_URL}/api/v1/customers/search?email=${encodeURIComponent(email)}`;
const response = await fetch(requestUrl, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
const errorResponse = await response.text();
return null;
}
const data = await response.json();
if (data && data.data && data.data.length 0) {
return data.data[0]; // ✅ Correctly accessing the first customer's ID
} else {
return null;
}
} catch (error) {
return null;
}
});
7.2 Backend — http-functions.js
import { ok, badRequest } from 'wix-http-functions';
import wixUsersBackend from 'wix-users-backend';
import wixCRMBackend from 'wix-crm-backend';
import wixData from 'wix-data';
import { fetch } from 'wix-fetch';
import { members } from "wix-members.v2";
import { contacts } from "wix-crm-backend";
import { Permissions, webMethod } from "wix-web-module";
import { elevate } from "wix-auth";
import { authentication } from "wix-members-backend";
import { triggeredEmails } from "wix-crm-backend";
/* =========================
CONFIG
========================= */
// WIX consts
const DEFAULT_PASSWORD = "TempPassword123";
// ⚠️ If your public site is not this domain, change to your Wix site URL:
const BASE_WIX_SITE_URL = "https://your-site.subscriptionflow.com";
// SubscriptionFlow base
const BASE_SF_URL = "https://your-site.subscriptionflow.com";
// ---- OAuth (Client Credentials) for SubscriptionFlow ----
// For production, store these in Wix Secrets Manager and read with wix-secrets-backend.
// Using constants here as per your snippet:
const CLIENT_ID = "YOUR_CLIENT_ID";
const CLIENT_SECRET = "YOUR_CLIENT_SECRET";
const TOKEN_URL = `${BASE_SF_URL}/oauth/token`;
let ACCESS_TOKEN = null;
let TOKEN_EXPIRY_MS = 0;
/**
* Get (and cache) an OAuth access token. Refreshes 1 minute before expiry.
*/
async function getAccessToken() {
const now = Date.now();
if (ACCESS_TOKEN && now "");
console.error("SF token request failed", { status: resp.status, statusText: resp.statusText, body: txt });
throw new Error("Cannot get SubscriptionFlow token");
}
const data = await resp.json();
ACCESS_TOKEN = data.access_token;
// expire 60s early
TOKEN_EXPIRY_MS = now + (data.expires_in * 1000) - 60_000;
return ACCESS_TOKEN;
}
/**
* Helper to call SubscriptionFlow with auto token + single retry on 401.
*/
async function sfFetch(path, options = {}) {
const doFetch = async (token) = fetch(`${BASE_SF_URL}${path}`, {
...options,
headers: {
"Authorization": `Bearer ${token}`,
...(options.headers || {})
}
});
let token = await getAccessToken();
let res = await doFetch(token);
// If token expired mid-flight, retry once
if (res.status === 401) {
ACCESS_TOKEN = null; // force refresh
token = await getAccessToken();
res = await doFetch(token);
}
return res;
}
/* =========================
PUBLIC METHODS (HTTP)
========================= */
export async function post_updateOrCreateMember(request) {
try {
const customerId = request?.query?.customer_id ??
new URLSearchParams(await request.body.text()).get("customer_id");
if (!customerId) {
return badRequest({ body: { error: "customer_id is required!" } });
}
return await updateOrCreateMember(customerId);
} catch (error) {
return badRequest({ body: { error: error.message } });
}
}
export async function post_updateOrCreateMemberNoEmail(request) {
try {
const customerId = request?.query?.customer_id ??
new URLSearchParams(await request.body.text()).get("customer_id");
if (!customerId) {
return badRequest({ body: { error: "customer_id is required!" } });
}
return await updateOrCreateMember(customerId, false);
} catch (error) {
return badRequest({ body: { error: error.message } });
}
}
export async function post_updateOrCreateMemberOnSubscriptionPaid(request) {
try {
const subscriptionId = request?.query?.subscription_id ??
new URLSearchParams(await request.body.text()).get("subscription_id");
if (!subscriptionId) {
return badRequest({ body: { error: "subscription_id is required!" } });
}
const subscriptionResponse = await sfFetch(`/api/v1/subscriptions/${subscriptionId}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
});
if (!subscriptionResponse.ok) {
const err = await subscriptionResponse.text().catch(() = "");
console.error("SF subscriptions fetch failed", {
status: subscriptionResponse.status,
statusText: subscriptionResponse.statusText,
body: err
});
return badRequest({ body: { error: "Failed to fetch subscription details from SubscriptionFlow", status: subscriptionResponse.status } });
}
const subscriptionData = await subscriptionResponse.json();
const subscription = subscriptionData?.data?.attributes;
const customerId = subscription?.customer?.id;
if (subscription?.payment_status !== 'Paid') {
return badRequest({ body: { error: "Subscription is not paid" } });
}
if (!customerId) {
return badRequest({ body: { error: "Failed to fetch customer details from SubscriptionFlow" } });
}
return await updateOrCreateMember(customerId);
} catch (error) {
return badRequest({ body: { error: error.message } });
}
}
/* =========================
HELPERS (Contacts/Members)
========================= */
/**
* Updates contact information in Wix CRM.
*/
async function updateContactInfo(contactId, contactData, extendedFields = {}) {
try {
const addresses = [];
if (contactData.billingAddress && (contactData.billingAddress.address || contactData.billingAddress.city)) {
addresses.push({
address: contactData.billingAddress.address,
city: contactData.billingAddress.city,
state: contactData.billingAddress.state,
postalCode: contactData.billingAddress.postalCode,
country: contactData.billingAddress.country
});
}
if (contactData.shippingAddress && (contactData.shippingAddress.address || contactData.shippingAddress.city)) {
addresses.push({
address: contactData.shippingAddress.address,
city: contactData.shippingAddress.city,
state: contactData.shippingAddress.state,
postalCode: contactData.shippingAddress.postalCode,
country: contactData.shippingAddress.country
});
}
const contactInfo = {
name: { first: contactData.firstName, last: contactData.lastName },
...(contactData.phone ? { phones: [{ phone: contactData.phone }] } : {}),
company: contactData.company,
...(addresses.length ? { addresses } : {}),
extendedFields
};
const contact = await getContactInformation(contactId);
const contactRevision = contact.revision;
return await updateContact(contactId, contactRevision, contactInfo);
} catch (error) {
console.error("Failed to update contact info:", error);
}
}
const getContactInformation = webMethod(
Permissions.Anyone,
async (id) = {
try {
const elevatedGetContact = elevate(contacts.getContact);
const contact = await elevatedGetContact(id);
return contact;
} catch (error) {
console.log(error);
}
},
);
const deleteContact = webMethod(
Permissions.Anyone,
async (contactId) = {
try {
const elevatedDeleteContact = elevate(contacts.deleteContact);
await elevatedDeleteContact(contactId);
console.log("Contact deleted.");
} catch (error) {
console.log(error);
}
},
);
const doRegistrationWithConformationEmail = webMethod(
Permissions.Anyone,
async (email, password, firstName, lastName) = {
const registrationOptions = {
contactInfo: { firstName, lastName, emails: [email] },
};
const registration = await authentication.register(email, password, registrationOptions);
console.log("Member is now registered with the site and pending approval");
const emailOptions = {
variables: {
name: firstName,
verifyLink: `${BASE_WIX_SITE_URL}/post-register?token=${registration.approvalToken}`,
},
};
triggeredEmails.emailMember("verifyRegistration", registration.member._id, emailOptions);
console.log("Confirmation email sent");
},
);
const doApproval = webMethod(Permissions.Anyone, async (token) = {
try {
const sessionToken = await authentication.approveByToken(token);
console.log("Member approved");
return { approved: true, sessionToken };
} catch (error) {
console.log("Member not approved");
return { approved: false, reason: error };
}
});
const sendSetPasswordEmail = webMethod(
Permissions.Anyone,
async (email, options) = {
try {
await authentication.sendSetPasswordEmail(email, options);
return true;
} catch (error) {
console.error("Error sending password reset email:", error);
throw new Error("Failed to send password reset email");
}
}
);
const updateContact = webMethod(Permissions.Anyone, async (contactId, contactRevision, contactInfo) = {
const contactIdentifiers = { contactId, revision: contactRevision };
const options = { allowDuplicates: false, suppressAuth: true };
return contacts
.updateContact(contactIdentifiers, contactInfo, options)
.then((updatedContact) = updatedContact)
.catch((error) = {
console.error(error);
});
});
/* =========================
CORE FLOW
========================= */
async function updateOrCreateMember(customerId, sendEmail = true) {
try {
// Fetch customer data from SubscriptionFlow API
const customerResponse = await sfFetch(`/api/v1/customers/${customerId}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
});
if (!customerResponse.ok) {
const err = await customerResponse.text().catch(() = "");
console.error("SF customers fetch failed", {
status: customerResponse.status,
statusText: customerResponse.statusText,
body: err
});
return badRequest({ body: { error: "Failed to fetch customer details from SubscriptionFlow", status: customerResponse.status } });
}
const customerData = await customerResponse.json();
const customer = customerData?.data?.attributes || {};
if (!customer.email) {
return badRequest({ body: { error: "Customer email is missing!" } });
}
// Extract customer details
const email = customer.email;
const firstName = customer.name ? customer.name.split(" ")[0] : "";
const lastName = customer.name ? customer.name.split(" ").slice(1).join(" ") : "";
const phone = customer.phone_number;
const company = customer.company;
const billingAddress = {
address: customer.billing_address_1,
city: customer.billing_city,
state: customer.billing_state,
postalCode: customer.billing_postal_code,
country: customer.billing_country
};
const shippingAddress = {
address: customer.shipping_address_1,
city: customer.shipping_city,
state: customer.shipping_state,
postalCode: customer.shipping_postal_code,
country: customer.shipping_country
};
const extendedFields = Object.fromEntries(
Object.entries({
// Add any mapped custom fields here, non-undefined only
// "custom.sf-customer-id": customer.id
}).filter(([_, value]) = value !== undefined)
);
// Check Wix Members table
const existingUser = await wixData.query("Members/FullData")
.eq("loginEmail", email)
.find();
if (existingUser.items.length 0) {
const userId = existingUser.items[0]?._id;
await updateContactInfo(userId, { firstName, lastName, phone, company, billingAddress, shippingAddress }, extendedFields);
return ok({ body: { message: "User already exists. Contact info updated.", userId } });
} else {
// Fallback: approved contact (already a member)
const options = { suppressAuth: true, suppressHooks: true };
const existingContact = await contacts.queryContacts()
.eq("info.emails.email", email)
.eq("info.extendedFields.members.membershipStatus", "APPROVED")
.find(options);
if (existingContact.items.length 0) {
const userId = existingContact.items[0]?._id;
await updateContactInfo(userId, { firstName, lastName, phone, company, billingAddress, shippingAddress }, extendedFields);
return ok({ body: { message: "User already exists. Contact info updated.", userId } });
}
}
// Register a new user in Wix
const newUser = await wixUsersBackend.register(email, DEFAULT_PASSWORD, {
contactInfo: { firstName, lastName, emails: [email] }
});
// Store additional info in Wix CRM
await updateContactInfo(newUser.user.id, { firstName, lastName, phone, company, billingAddress, shippingAddress }, extendedFields);
if (sendEmail) {
await sendSetPasswordEmail(email);
}
return ok({ body: { message: "New member created successfully.", userId: newUser.user.id } });
} catch (error) {
return badRequest({ body: { error: error.message } });
}
}
/* =========================
HEALTH CHECK
========================= */
export function get_health() {
return ok({ body: { status: "ok" } });
}
7.3 Custom Element — Product Page
subscriptionflow-product-buy-now.js
// Debug toggle
const DEBUG_SF = true;
function dlog(label, color, ...args) {
if (!DEBUG_SF) return;
console.log(`%csf-your-site: ${label}`, `color:${color};font-weight:600;`, ...args);
}
class SubscriptionflowBuyNowButtonAdd extends HTMLElement {
static get observedAttributes() {
return ['bunowbutton'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.intervalId = null;
this.fullUrl = null;
dlog("BuyNow Constructor initialized", "purple");
}
connectedCallback() {
dlog("BuyNow component connected", "green");
this.startButtonPolling();
}
disconnectedCallback() {
dlog("BuyNow component disconnected", "red");
if (this.intervalId) clearInterval(this.intervalId);
}
attributeChangedCallback(name, oldValue, newValue) {
dlog("Attribute changed", "orange", { name, oldValue, newValue });
if (name === 'bunowbutton') {
if (newValue && newValue !== 'null') {
this.fullUrl = newValue;
dlog("SF URL received", "green", this.fullUrl);
dlog("Product URL detected", "blue", this.fullUrl);
this.showCustomButton();
} else {
dlog("Subscription disabled or URL cleared", "gray");
this.fullUrl = null;
this.restoreWixButton();
}
}
}
startButtonPolling() {
dlog("Starting Buy Now button polling", "blue");
this.intervalId = setInterval(() = {
const checkoutBtn = document.querySelector('[aria-label="Buy Now"]');
if (checkoutBtn) {
dlog("Buy Now button found", "green", checkoutBtn);
clearInterval(this.intervalId);
this.intervalId = null;
if (this.fullUrl) {
dlog("URL already available → showing SF button", "blue");
this.showCustomButton();
} else {
dlog("Waiting for URL attribute...", "orange");
}
} else {
dlog("Waiting for Buy Now button...", "gray");
}
}, 500);
}
showCustomButton() {
try {
dlog("showCustomButton() triggered", "purple");
const checkoutBtn = document.querySelector('[aria-label="BUY NOW"]');
if (!checkoutBtn) {
dlog("Buy Now button NOT found in DOM", "red");
return;
}
if (!this.fullUrl) {
dlog("No SF URL available", "red");
return;
}
// Hide original
checkoutBtn.style.display = 'none';
dlog("Original Buy Now hidden", "blue");
// Remove existing
const existing = document.querySelector('#pbuy-now-link');
if (existing) {
existing.remove();
dlog("Old SF button removed", "orange");
}
// Create button
const link = document.createElement('a');
link.id = 'pbuy-now-link';
link.textContent = 'BUY NOW';
link.href = this.fullUrl;
link.target = '_self';
link.style.cssText = `
display: flex;
padding: 14px 16px !important;
justify-content: center;
background-color: #e48922;
color: #242424;
border-radius: 0px;
font-family: montserrat, sans-serif;
font-size: 16px !important;
font-weight: 400 ;
text-align: center;
text-decoration: none;
cursor: pointer;
`;
link.addEventListener('click', () = {
dlog("SF Buy Now clicked", "green", this.fullUrl);
window.parent.postMessage({
type: 'subscribeClicked',
url: this.fullUrl
}, '*');
});
checkoutBtn.parentNode.insertBefore(link, checkoutBtn.nextSibling);
dlog("SF Buy Now button inserted successfully", "green");
} catch (err) {
console.error("sf-your-site: showCustomButton error:", err);
}
}
restoreWixButton() {
console.log("sf-your-site: Restoring original Buy Now button");
const existing = document.querySelector('#pbuy-now-link');
if (existing) {
existing.remove();
console.log("sf-your-site: SF button removed");
}
const tryRestore = () = {
const checkoutBtn =
document.querySelector('.buy-now-button') ||
document.querySelector('[aria-label="BUY NOW"]');
if (checkoutBtn) {
checkoutBtn.style.display = '';
checkoutBtn.style.visibility = 'visible';
checkoutBtn.style.opacity = '1';
console.log("sf-your-site: Buy Now restored");
return true;
}
return false;
};
// 🔁 Try immediately
if (!tryRestore()) {
console.log("sf-your-site: Button not found → retrying...");
// 🔁 Retry multiple times (Wix render delay fix)
let attempts = 0;
const retryInterval = setInterval(() = {
if (tryRestore() || attempts 10) {
clearInterval(retryInterval);
console.log("sf-your-site: Restore attempts finished");
}
attempts++;
}, 300);
}
}
}
customElements.define('subscriptionflow-product-buy-now', SubscriptionflowBuyNowButtonAdd);
7.4 Custom Element — Cart Page
subscriptionflow-button-add.js
// Debug toggle
const DEBUG_SF = true;
function dlog(label, color, ...args) {
if (!DEBUG_SF) return;
console.log(
`%csf-your-site: ${label}`,
`color:${color};font-weight:600;`,
...args,
);
}
function dgroup(label, color) {
if (!DEBUG_SF) return { end: () = {} };
console.groupCollapsed(
`%csf-your-site: ${label}`,
`color:${color};font-weight:700;`,
);
return { end: () = console.groupEnd() };
}
class SubscriptionflowButtonAdd extends HTMLElement {
static get observedAttributes() {
return ["customdatacart", "customerdatjsons"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.cart = null;
this.intervalId = null;
this.customerData = null;
this.activeCoupon = null;
dlog("Constructor initialized", "purple");
}
connectedCallback() {
dlog("Connected to DOM", "green");
this.startButtonPolling();
this.updateButtonLogic();
}
disconnectedCallback() {
dlog("Disconnected from DOM", "red");
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
attributeChangedCallback(name, oldValue, newValue) {
dlog(`Attribute changed → ${name}`, "orange", { oldValue, newValue });
if (name === "customdatacart" && newValue !== oldValue) {
try {
this.cart = JSON.parse(newValue);
dlog("Cart parsed successfully", "green", this.cart);
// ✅ ADD THIS
this.activeCoupon = this.resolveCouponFromWix(this.cart);
dlog("Resolved coupon", "teal", this.activeCoupon);
this.updateButtonLogic();
} catch (err) {
console.error("sf-your-site: Cart parse error:", err);
}
}
if (name === "customerdatjsons" && newValue !== oldValue) {
try {
this.customerData = JSON.parse(newValue);
dlog("Customer data parsed", "green", this.customerData);
} catch (err) {
this.customerData = newValue;
dlog("Customer data raw fallback", "yellow", this.customerData);
}
}
}
getNonManagedVariantId(productId) {
dlog("Generating fallback variant ID", "blue", productId);
if (typeof productId !== "string" || productId.length !== 36) {
dlog("Invalid productId for variant", "red", productId);
return null;
}
return productId.substring(0, 14) + "vn01" + productId.substring(18);
}
startButtonPolling() {
dlog("Starting checkout button polling", "blue");
this.intervalId = setInterval(() = {
const checkoutBtn = document.querySelector(
'[data-wix-checkout-button="CheckoutButtonDataHook.button"]',
);
if (checkoutBtn) {
dlog("Checkout button found", "green", checkoutBtn);
clearInterval(this.intervalId);
this.intervalId = null;
this.updateButtonLogic();
} else {
dlog("Waiting for checkout button...", "gray");
}
}, 500);
}
addOrReplaceParams(url, params) {
try {
const u = new URL(url);
Object.entries(params).forEach(([k, v]) = {
if (v === undefined || v === null || v === "") return;
u.searchParams.set(k, String(v));
});
return u.toString();
} catch (e) {
return url;
}
}
resolveCouponFromWix(cartData) {
try {
const d1 = cartData?.appliedDiscounts?.find?.((d) = d?.coupon?.code)
?.coupon?.code;
if (d1) return String(d1);
const d2 =
cartData?.discounts?.appliedCoupons?.[0]?.code ||
cartData?.discounts?.appliedCoupons?.[0]?.couponCode;
if (d2) return String(d2);
const d3 =
cartData?.totals?.coupon?.code || cartData?.totals?.coupon?.couponCode;
if (d3) return String(d3);
return null;
} catch (e) {
console.warn("sf-your-site: coupon resolve failed", e);
return null;
}
}
updateButtonLogic() {
const grp = dgroup("updateButtonLogic()", "purple");
try {
const checkoutBtn = document.querySelector(
'[data-wix-checkout-button="CheckoutButtonDataHook.button"]',
);
if (!checkoutBtn) {
dlog("Checkout button NOT found", "red");
grp.end();
return;
}
if (!this.cart) {
dlog("Cart not available yet", "orange");
grp.end();
return;
}
const lineItems = this.cart.lineItems || [];
dlog("Cart line items", "blue", lineItems);
const existing = document.querySelector("#subscribe-now-link");
const targetItems = lineItems;
dlog("Filtered target items", "blue", targetItems);
if (!targetItems.length) {
dlog("No target product → normal checkout", "green");
checkoutBtn.style.display = "";
if (existing) existing.remove();
grp.end();
return;
}
const purchaseOptions = targetItems.map(
(item) =
item?.catalogReference?.options?.options?.["Purchase Option"] || "",
);
dlog("Purchase options", "blue", purchaseOptions);
const allOneTime = purchaseOptions.every(
(option) = !option || option === "One-Time",
);
if (allOneTime) {
dlog("All items One-Time → normal checkout", "green");
checkoutBtn.style.display = "";
if (existing) existing.remove();
grp.end();
return;
}
dlog("Subscription detected → switching to SF checkout", "purple");
checkoutBtn.style.display = "none";
let url_param_list = "";
let counter = 0;
targetItems.forEach((item) = {
const wixProductID = item.catalogReference?.catalogItemId || "";
const wixVariantID = item.catalogReference?.options?.variantId || "";
const quantity = item.quantity;
let sf_product = wixProductID;
let sf_variant =
wixVariantID &&
wixVariantID !== "00000000-0000-0000-0000-000000000000"
? wixVariantID
: this.getNonManagedVariantId(wixProductID);
dlog("Item mapping", "blue", {
sf_product,
sf_variant,
quantity,
});
if (sf_product && sf_variant) {
url_param_list += `items[${counter}][pr]=${encodeURIComponent(sf_product)}&items[${counter}][pl]=${encodeURIComponent(sf_variant)}&items[${counter}][q]=${encodeURIComponent(quantity)}&`;
counter++;
}
});
let customerParams = "";
if (this.customerData) {
dlog("Adding customer data to URL", "blue", this.customerData);
const fullName = this.customerData.name || "";
/*
// ✅ ADD HERE
const shippingType = this.cart?.shippingInfo?.type || "flat";
const shippingAmount = this.cart?.totals?.shipping || 0;
dlog("Shipping Data", "teal", {
shippingType,
shippingAmount,
cartShippingInfo: this.cart?.shippingInfo,
cartTotals: this.cart?.totals
});
*/
const queryParams = new URLSearchParams({
ai_email: this.customerData.email || "",
ai_firstName: "",
ai_lastName: fullName,
ai_phone: this.customerData.phone_number || "",
ai_billing_country: this.customerData.billing_country || "",
ai_shipping_country: this.customerData.shipping_country || "",
});
customerParams = queryParams.toString();
}
// ✅ Get selected shipping code
const selectedShippingCode =
this.cart?.selectedShippingOption?.code || "";
dlog("Selected Shipping Code", "teal", selectedShippingCode);
// ✅ Condition check
let shippingParam = "";
if (selectedShippingCode === "fff0f7bb2cdc4b10a7f9596c59fde242") {
shippingParam = "&shipping_method=Priority+Shipping";
dlog("Priority Shipping Applied", "green");
} else {
dlog("No Shipping Param Applied", "yellow");
}
// ✅ Final URL
// const fullUrl =
// `https://your-site.subscriptionflow.com/en/hosted-page/commerceflow?` +
// `${url_param_list}${customerParams ? "&" + customerParams : ""}` +
// `${shippingParam}` +
// `&cart=${encodeURIComponent("https://www.your-site.com")}`;
// const fullUrl =
// `https://your-site.subscriptionflow.com/en/hosted-page/commerceflow?` +
// `${url_param_list}${customerParams ? "&" + customerParams : ""}` +
// `&cart=${encodeURIComponent("https://www.your-site.com")}`;
let fullUrl =
`https://your-site.subscriptionflow.com/en/hosted-page/commerceflow?` +
`${url_param_list}${customerParams ? "&" + customerParams : ""}` +
`${shippingParam}` +
`&cart=${encodeURIComponent("https://www.your-site.com/cart-page")}`;
// ✅ ADD THIS BLOCK
if (fullUrl && this.activeCoupon) {
fullUrl = this.addOrReplaceParams(fullUrl, {
coupon_code: this.activeCoupon,
});
dlog("Coupon applied to URL", "green", this.activeCoupon);
}
dlog("Final SF URL", "green", fullUrl);
if (existing) {
existing.href = fullUrl;
dlog("Updated existing subscribe link", "green");
} else {
const link = document.createElement("a");
link.id = "subscribe-now-link";
link.textContent = "Checkout";
link.href = fullUrl;
link.target = "_self";
link.style.cssText = `
display: flex;
padding: 10px 20px;
margin-top: 10px;
justify-content: center;
background-color: #1e4383;
color: white;
border-radius: 0px;
font-size: 16px;
text-align: center;
text-decoration: none;
cursor: pointer;
`;
checkoutBtn.parentNode.insertBefore(link, checkoutBtn.nextSibling);
dlog("Created new subscribe button", "green");
}
} catch (e) {
console.error("sf-your-site: updateButtonLogic error:", e);
}
grp.end();
}
}
customElements.define("subscriptionflow-button-add", SubscriptionflowButtonAdd);
7.5 Product Page Code
import { session } from 'wix-storage-frontend'
import wixUsers from 'wix-users';
import { fetchCustomerDetail } from 'backend/siteApi.web';
$w.onReady(async () = {
// Works even if your element ID isn't the default
const productComp = $w('#productPage1'); // or $w('#productPage1')
const product = await productComp.getProduct();
console.log('Full product object:', product); // View in Dev Tools (Preview/Live)
});
// Buy Now Logic
$w.onReady(async function () {
//SubscriptionFlow code
const cartUrl = "https://www.your-site.com/cart-page";
const baseUrl = "https://your-site.subscriptionflow.com/en/hosted-page/commerceflow";
if (wixUsers.currentUser.loggedIn) {
const email = await wixUsers.currentUser.getEmail();
// Check if data is already in session to avoid repeated calls
if (email && !session.getItem("customerData")) {
const customerData = await fetchCustomerDetail(email);
if (customerData) {
session.setItem("customerData", JSON.stringify(customerData));
console.log("✅ SubscriptionFlow data saved in session:", customerData);
} else {
console.log("❌ No customer data found for email:", email);
}
}
}
function handleSubscriptionCheck() {
Promise.all([
$w("#productPage1").getProduct(),
$w("#productPage1").getSelectedVariantId(),
$w("#productPage1").getSelectedChoices(),
$w("#productPage1").getQuantity()
])
.then(([product, variantId, selectedChoices, quantity]) = {
const productId = product._id;
const subscriptionValue = selectedChoices?.["Purchase Option"];
console.log("sf-your-site: Selected Choices:", selectedChoices);
console.log("sf-your-site: Subscription Value:", subscriptionValue);
const isSubscription =
subscriptionValue &&
!/one[-\s]?time/i.test(subscriptionValue);
console.log("sf-your-site: Is Subscription:", isSubscription);
if (isSubscription) {
let customerParams = '';
const sessionCustomerRaw = session.getItem("customerData");
if (sessionCustomerRaw) {
try {
const customerData = JSON.parse(sessionCustomerRaw);
const [firstName, ...lastNameParts] = customerData.name?.split(" ") || [];
const lastName = lastNameParts.join(" ");
const fullName = `${firstName || ""} ${lastName || ""}`.trim();
const queryParams = new URLSearchParams({
ai_email: customerData.email || "",
ai_firstName: "",
ai_lastName: fullName || "",
ai_phone: customerData.phone_number || "",
ai_billing_address1: customerData.billing_address_1 || "",
ai_shipping_address1: customerData.shipping_address_1 || "",
ai_billing_address2: customerData.billing_address_2 || "",
ai_shipping_address2: customerData.shipping_address_2 || "",
ai_billing_address3: customerData.billing_address_3 || "",
ai_shipping_address3: customerData.shipping_address_3 || "",
ai_billing_zip: customerData.billing_postal_code || "",
ai_shipping_zip: customerData.shipping_postal_code || "",
ai_billing_state: customerData.billing_state || "",
ai_shipping_state: customerData.shipping_state || "",
ai_billing_city: customerData.billing_city || "",
ai_shipping_city: customerData.shipping_city || "",
ai_billing_country: customerData.billing_country || "",
ai_shipping_country: customerData.shipping_country || ""
});
customerParams = queryParams.toString();
} catch (err) {
console.error("❌ Error parsing customerData", err);
}
} else {
const queryParams_2 = new URLSearchParams({
ai_billing_country: "US",
ai_shipping_country: "US",
});
customerParams = queryParams_2.toString();
}
const urlParamList = `items[0][pr]=${productId}&items[0][pl]=${variantId}&items[0][q]=${quantity}`;
const fullUrl = `${baseUrl}?${urlParamList}${customerParams ? '&' + customerParams : ''}&cart=${cartUrl}`;
$w('#customElement1').setAttribute('bunowbutton', fullUrl); // ✅ send to custom element
} else {
$w('#customElement1').setAttribute('bunowbutton', ''); // ❌ not subscription → restore Wix Buy Now
}
})
.catch((error) = {
console.error("⚠️ Error getting product info:", error);
$w('#customElement1').setAttribute('bunowbutton', ''); // fallback restore
});
}
// ✅ Trigger on Frequency choice
$w("#productPage1").onChoiceSelected((selectedChoices) = {
if (selectedChoices?.["Purchase Option"]) {
handleSubscriptionCheck();
} else {
console.log('This is not a subscription');
$w('#customElement1').setAttribute('bunowbutton', '');
}
});
// ✅ Trigger on quantity change
$w("#productPage1").onQuantityChanged(() = {
handleSubscriptionCheck();
});
});
7.6 Cart Page Code
import wixEcomFrontend from 'wix-ecom-frontend';
import wixEcomBackend from 'wix-ecom-backend';
import { fetchCustomerDetail, myGetCurrentCartFunction } from 'backend/siteApi.web';
import { session } from 'wix-storage';
import wixUsers from 'wix-users';
$w.onReady(async function () {
console.log("sf-your-site: Page Ready Triggered");
if (wixUsers.currentUser.loggedIn) {
console.log("sf-your-site: User is logged in");
const email = await wixUsers.currentUser.getEmail();
console.log("sf-your-site: User email fetched:", email);
if (email && !session.getItem("customerData")) {
console.log("sf-your-site: Fetching customer data...");
const customerData = await fetchCustomerDetail(email);
if (customerData) {
session.setItem("customerData", JSON.stringify(customerData));
console.log("sf-your-site: Customer data saved in session:", customerData);
} else {
console.log("sf-your-site: No customer data found for email:", email);
}
} else {
console.log("sf-your-site: Customer data already exists in session or email missing");
}
} else {
console.log("sf-your-site: User NOT logged in");
}
console.log("sf-your-site: Calling updateCustomElementCart()");
updateCustomElementCart();
wixEcomFrontend.onCartChange(() = {
console.log("sf-your-site: Cart change detected");
updateCustomElementCart();
});
});
async function updateCustomElementCart() {
try {
console.log("sf-your-site: updateCustomElementCart() started");
const cartData = await myGetCurrentCartFunction();
console.log("sf-your-site: Cart data received:", cartData);
const cartItems = cartData?.lineItems || [];
console.log("sf-your-site: Cart items count:", cartItems.length);
if (cartItems.length 0) {
const session_customerData = session.getItem("customerData");
console.log("sf-your-site: Session customer data:", session_customerData);
const cartJson = JSON.stringify(cartData);
console.log("sf-your-site: Setting cart data to custom element");
$w('#customElement1').setAttribute('customdatacart', cartJson);
if (session_customerData) {
console.log("sf-your-site: Setting customer data to custom element");
$w('#customElement1').setAttribute('customerdatjsons', session_customerData);
} else {
console.log("sf-your-site: No customer data found in session");
}
} else {
console.log("sf-your-site: No items in cart");
}
} catch (error) {
console.error("sf-your-site: ERROR in updateCustomElementCart:", error);
}
}
7.7 Final Notes
- Ensure all file names match exactly
- Verify custom element IDs in Wix Editor
- Test both subscription and one-time flows
- Check webhook endpoints after deployment
Comments
0 comments
Please sign in to leave a comment.