diff --git a/EOF b/EOF
new file mode 100644
index 0000000..e69de29
diff --git a/features.md b/features.md
new file mode 100644
index 0000000..0ea114f
--- /dev/null
+++ b/features.md
@@ -0,0 +1,3 @@
+programmatic seo
+pop up and exit pop to grow emaillist connected with resend and mautic. want to always have my list growing and owned by me on my server
+abandoned cart setup with sequences to get people back
diff --git a/public/debug-op.js b/public/debug-op.js
new file mode 100644
index 0000000..2138ee1
--- /dev/null
+++ b/public/debug-op.js
@@ -0,0 +1,40 @@
+// OpenPanel Debug Script
+// Run this in browser console to test OpenPanel
+
+(function debugOpenPanel() {
+ console.log('=== OpenPanel Debug ===');
+
+ // Check if OpenPanel is loaded
+ if (typeof window.op === 'undefined') {
+ console.error('โ OpenPanel SDK not loaded (window.op is undefined)');
+ console.log('Script URL should be:', 'https://op.nodecrew.me/op1.js');
+ return;
+ }
+
+ console.log('โ
OpenPanel SDK loaded');
+ console.log('window.op:', window.op);
+
+ // Check client ID
+ const clientId = window.op._clientId || 'not set';
+ console.log('Client ID:', clientId);
+
+ // Try to track an event
+ console.log('Attempting to track test event...');
+ window.op.track('debug_test', { source: 'console', timestamp: new Date().toISOString() })
+ .then(() => console.log('โ
Track successful'))
+ .catch(err => console.error('โ Track failed:', err));
+
+ // Check network requests
+ console.log('');
+ console.log('Check Network tab for requests to:');
+ console.log('- https://manoonoils.com/api/op/track');
+ console.log('- https://op.nodecrew.me/api/track');
+
+ // Common issues
+ console.log('');
+ console.log('Common issues:');
+ console.log('1. Ad blockers (try disabling uBlock/AdBlock)');
+ console.log('2. CORS errors (check console for red errors)');
+ console.log('3. Do Not Track enabled in browser');
+ console.log('4. Private/Incognito mode (some blockers active)');
+})();
diff --git a/scripts/test-checkout-shipping.js b/scripts/test-checkout-shipping.js
new file mode 100644
index 0000000..b974c59
--- /dev/null
+++ b/scripts/test-checkout-shipping.js
@@ -0,0 +1,310 @@
+#!/usr/bin/env node
+/**
+ * Test script for checkout shipping cost calculation
+ * Creates a checkout via API and verifies totalPrice includes shipping
+ */
+
+const SALEOR_API_URL = process.env.NEXT_PUBLIC_SALEOR_API_URL || 'https://api.manoonoils.com/graphql/';
+
+// Test data
+const TEST_VARIANT_ID = 'UHJvZHVjdFZhcmlhbnQ6Mjk0'; // Replace with actual variant ID
+const TEST_EMAIL = 'test@example.com';
+
+const TEST_SHIPPING_ADDRESS = {
+ firstName: 'Test',
+ lastName: 'User',
+ streetAddress1: '123 Test Street',
+ city: 'Belgrade',
+ postalCode: '11000',
+ country: 'RS',
+ phone: '+38160123456'
+};
+
+async function saleorFetch(query, variables = {}, token = null) {
+ const headers = {
+ 'Content-Type': 'application/json',
+ };
+
+ if (token) {
+ headers['Authorization'] = `JWT ${token}`;
+ }
+
+ const response = await fetch(SALEOR_API_URL, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({ query, variables }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result.errors) {
+ throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
+ }
+
+ return result.data;
+}
+
+async function testCheckoutWithShipping() {
+ console.log('๐งช Testing checkout shipping cost calculation...\n');
+
+ try {
+ // Step 1: Create checkout
+ console.log('Step 1: Creating checkout...');
+ const checkoutCreateMutation = `
+ mutation CheckoutCreate($input: CheckoutCreateInput!) {
+ checkoutCreate(input: $input) {
+ checkout {
+ id
+ token
+ totalPrice {
+ gross {
+ amount
+ currency
+ }
+ }
+ subtotalPrice {
+ gross {
+ amount
+ currency
+ }
+ }
+ }
+ errors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const checkoutResult = await saleorFetch(checkoutCreateMutation, {
+ input: {
+ channel: 'default-channel',
+ email: TEST_EMAIL,
+ lines: [],
+ languageCode: 'SR'
+ }
+ });
+
+ if (checkoutResult.checkoutCreate.errors?.length > 0) {
+ throw new Error(`Checkout creation failed: ${checkoutResult.checkoutCreate.errors[0].message}`);
+ }
+
+ const checkout = checkoutResult.checkoutCreate.checkout;
+ console.log(`โ
Checkout created: ${checkout.id}`);
+ console.log(` Token: ${checkout.token}`);
+ console.log(` Initial total: ${checkout.totalPrice.gross.amount} ${checkout.totalPrice.gross.currency}\n`);
+
+ // Step 2: Add product to checkout
+ console.log('Step 2: Adding product to checkout...');
+ const linesAddMutation = `
+ mutation CheckoutLinesAdd($checkoutId: ID!, $lines: [CheckoutLineInput!]!) {
+ checkoutLinesAdd(checkoutId: $checkoutId, lines: $lines) {
+ checkout {
+ id
+ totalPrice {
+ gross {
+ amount
+ currency
+ }
+ }
+ subtotalPrice {
+ gross {
+ amount
+ currency
+ }
+ }
+ lines {
+ id
+ quantity
+ totalPrice {
+ gross {
+ amount
+ }
+ }
+ }
+ }
+ errors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ // First, let's query for available products to get a real variant ID
+ console.log(' Querying available products...');
+ const productsQuery = `
+ query Products {
+ products(channel: "default-channel", first: 1) {
+ edges {
+ node {
+ id
+ name
+ variants {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ const productsResult = await saleorFetch(productsQuery);
+ const product = productsResult.products.edges[0]?.node;
+
+ if (!product || !product.variants?.[0]) {
+ throw new Error('No products found in store');
+ }
+
+ const variantId = product.variants[0].id;
+ console.log(` Product: ${product.name}, Variant: ${product.variants[0].name}`);
+
+ const linesResult = await saleorFetch(linesAddMutation, {
+ checkoutId: checkout.id,
+ lines: [{ variantId, quantity: 1 }]
+ });
+
+ if (linesResult.checkoutLinesAdd.errors?.length > 0) {
+ throw new Error(`Adding lines failed: ${linesResult.checkoutLinesAdd.errors[0].message}`);
+ }
+
+ const checkoutWithLines = linesResult.checkoutLinesAdd.checkout;
+ const productTotal = checkoutWithLines.totalPrice.gross.amount;
+ console.log(`โ
Product added (qty: 1)`);
+ console.log(` Product total: ${productTotal} RSD\n`);
+
+ // Step 3: Set shipping address
+ console.log('Step 3: Setting shipping address...');
+ const shippingAddressMutation = `
+ mutation CheckoutShippingAddressUpdate($checkoutId: ID!, $shippingAddress: AddressInput!) {
+ checkoutShippingAddressUpdate(checkoutId: $checkoutId, shippingAddress: $shippingAddress) {
+ checkout {
+ id
+ shippingMethods {
+ id
+ name
+ price {
+ amount
+ currency
+ }
+ }
+ }
+ errors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const shippingResult = await saleorFetch(shippingAddressMutation, {
+ checkoutId: checkout.id,
+ shippingAddress: TEST_SHIPPING_ADDRESS
+ });
+
+ if (shippingResult.checkoutShippingAddressUpdate.errors?.length > 0) {
+ throw new Error(`Setting shipping address failed: ${shippingResult.checkoutShippingAddressUpdate.errors[0].message}`);
+ }
+
+ const availableMethods = shippingResult.checkoutShippingAddressUpdate.checkout.shippingMethods;
+ console.log(`โ
Shipping address set`);
+ console.log(` Available shipping methods: ${availableMethods.length}`);
+
+ if (availableMethods.length === 0) {
+ console.log(' โ ๏ธ No shipping methods available for this address/region');
+ return;
+ }
+
+ availableMethods.forEach((method, i) => {
+ console.log(` [${i + 1}] ${method.name}: ${method.price.amount} ${method.price.currency}`);
+ });
+ console.log('');
+
+ // Step 4: Set shipping method
+ const selectedMethod = availableMethods[0];
+ console.log(`Step 4: Selecting shipping method: ${selectedMethod.name} (${selectedMethod.price.amount} RSD)...`);
+
+ const shippingMethodMutation = `
+ mutation CheckoutShippingMethodUpdate($checkoutId: ID!, $shippingMethodId: ID!) {
+ checkoutShippingMethodUpdate(checkoutId: $checkoutId, shippingMethodId: $shippingMethodId) {
+ checkout {
+ id
+ totalPrice {
+ gross {
+ amount
+ currency
+ }
+ }
+ subtotalPrice {
+ gross {
+ amount
+ currency
+ }
+ }
+ shippingPrice {
+ gross {
+ amount
+ currency
+ }
+ }
+ }
+ errors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const methodResult = await saleorFetch(shippingMethodMutation, {
+ checkoutId: checkout.id,
+ shippingMethodId: selectedMethod.id
+ });
+
+ if (methodResult.checkoutShippingMethodUpdate.errors?.length > 0) {
+ throw new Error(`Setting shipping method failed: ${methodResult.checkoutShippingMethodUpdate.errors[0].message}`);
+ }
+
+ const finalCheckout = methodResult.checkoutShippingMethodUpdate.checkout;
+ const subtotal = finalCheckout.subtotalPrice.gross.amount;
+ const shipping = finalCheckout.shippingPrice.gross.amount;
+ const finalTotal = finalCheckout.totalPrice.gross.amount;
+ const expectedTotal = subtotal + shipping;
+
+ console.log(`โ
Shipping method set`);
+ console.log(` Subtotal: ${subtotal} RSD`);
+ console.log(` Shipping: ${shipping} RSD`);
+ console.log(` Total: ${finalTotal} RSD`);
+ console.log(` Expected: ${expectedTotal} RSD`);
+ console.log('');
+
+ // Verification
+ console.log('๐ VERIFICATION:');
+ if (finalTotal === expectedTotal) {
+ console.log('โ
PASS: Total includes shipping cost correctly');
+ console.log(` ${subtotal} + ${shipping} = ${finalTotal}`);
+ } else {
+ console.log('โ FAIL: Total does NOT include shipping cost');
+ console.log(` Expected: ${expectedTotal}, Got: ${finalTotal}`);
+ console.log(` Difference: ${expectedTotal - finalTotal}`);
+ }
+
+ // Cleanup - delete checkout
+ console.log('\n๐งน Cleaning up test checkout...');
+ // Note: Checkout deletion requires admin permissions
+ console.log(` Checkout ID for manual cleanup: ${checkout.id}`);
+
+ } catch (error) {
+ console.error('\nโ Test failed:', error.message);
+ process.exit(1);
+ }
+}
+
+// Run the test
+testCheckoutWithShipping();
diff --git a/scripts/test-frontend-checkout.js b/scripts/test-frontend-checkout.js
new file mode 100644
index 0000000..e69de29
diff --git a/scripts/test-frontend.mjs b/scripts/test-frontend.mjs
new file mode 100644
index 0000000..cce9a0d
--- /dev/null
+++ b/scripts/test-frontend.mjs
@@ -0,0 +1,137 @@
+const SALEOR_API_URL = 'https://api.manoonoils.com/graphql/';
+
+async function saleorFetch(query, variables = {}) {
+ const response = await fetch(SALEOR_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query, variables }),
+ });
+ const result = await response.json();
+ if (result.errors) {
+ console.error('GraphQL Errors:', JSON.stringify(result.errors, null, 2));
+ throw new Error(JSON.stringify(result.errors));
+ }
+ return result.data;
+}
+
+async function test() {
+ // Create checkout
+ const createResult = await saleorFetch(`
+ mutation {
+ checkoutCreate(input: {
+ channel: "default-channel"
+ email: "test@test.com"
+ lines: [{ variantId: "UHJvZHVjdFZhcmlhbnQ6Mjk0", quantity: 1 }]
+ languageCode: SR
+ }) {
+ checkout {
+ id
+ token
+ totalPrice { gross { amount } }
+ subtotalPrice { gross { amount } }
+ }
+ errors {
+ field
+ message
+ code
+ }
+ }
+ }
+ `);
+
+ if (createResult.checkoutCreate.errors?.length > 0) {
+ console.error('Checkout creation errors:', createResult.checkoutCreate.errors);
+ throw new Error('Checkout creation failed');
+ }
+ if (!createResult.checkoutCreate.checkout) {
+ console.error('Create result:', createResult);
+ throw new Error('Checkout creation returned null');
+ }
+ const checkout = createResult.checkoutCreate.checkout;
+ const token = checkout.token;
+
+ console.log('Created checkout:');
+ console.log(' ID:', checkout.id);
+ console.log(' Token:', token);
+ console.log(' Initial Total:', checkout.totalPrice.gross.amount);
+
+ // Set address
+ await saleorFetch(`
+ mutation {
+ checkoutShippingAddressUpdate(
+ checkoutId: "${checkout.id}"
+ shippingAddress: {
+ firstName: "Test"
+ lastName: "User"
+ streetAddress1: "123 Street"
+ city: "Belgrade"
+ postalCode: "11000"
+ country: "RS"
+ phone: "+38160123456"
+ }
+ ) {
+ checkout {
+ shippingMethods { id name price { amount } }
+ }
+ }
+ }
+ `);
+
+ // Query by token (what refreshCheckout does)
+ const tokenQuery = await saleorFetch(`
+ query {
+ checkout(token: "${token}") {
+ id
+ token
+ totalPrice { gross { amount } }
+ subtotalPrice { gross { amount } }
+ shippingPrice { gross { amount } }
+ shippingMethods { id name price { amount } }
+ }
+ }
+ `);
+
+ console.log('\nQuery by token (before shipping method):');
+ console.log(' Total:', tokenQuery.checkout.totalPrice.gross.amount);
+ console.log(' Subtotal:', tokenQuery.checkout.subtotalPrice.gross.amount);
+ console.log(' Shipping:', tokenQuery.checkout.shippingPrice.gross.amount);
+ console.log(' Methods:', tokenQuery.checkout.shippingMethods.length);
+
+ if (tokenQuery.checkout.shippingMethods.length > 0) {
+ const methodId = tokenQuery.checkout.shippingMethods[0].id;
+
+ // Set shipping method
+ await saleorFetch(`
+ mutation {
+ checkoutShippingMethodUpdate(
+ checkoutId: "${checkout.id}"
+ shippingMethodId: "${methodId}"
+ ) {
+ checkout {
+ totalPrice { gross { amount } }
+ subtotalPrice { gross { amount } }
+ shippingPrice { gross { amount } }
+ }
+ }
+ }
+ `);
+
+ // Query by token again (what should happen after refreshCheckout)
+ const afterMethod = await saleorFetch(`
+ query {
+ checkout(token: "${token}") {
+ totalPrice { gross { amount } }
+ subtotalPrice { gross { amount } }
+ shippingPrice { gross { amount } }
+ }
+ }
+ `);
+
+ console.log('\nQuery by token (AFTER shipping method):');
+ console.log(' Total:', afterMethod.checkout.totalPrice.gross.amount);
+ console.log(' Subtotal:', afterMethod.checkout.subtotalPrice.gross.amount);
+ console.log(' Shipping:', afterMethod.checkout.shippingPrice.gross.amount);
+ }
+}
+
+test().catch(console.error);
diff --git a/scripts/test-full-checkout-flow.js b/scripts/test-full-checkout-flow.js
new file mode 100644
index 0000000..5f81216
--- /dev/null
+++ b/scripts/test-full-checkout-flow.js
@@ -0,0 +1,254 @@
+#!/usr/bin/env node
+/**
+ * Complete API test simulating frontend checkout flow
+ * Tests every step the frontend takes
+ */
+
+const SALEOR_API_URL = 'https://api.manoonoils.com/graphql/';
+
+async function saleorFetch(query, variables = {}) {
+ const response = await fetch(SALEOR_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query: query.replace(/\n\s*/g, ' '), variables }),
+ });
+ const result = await response.json();
+ if (result.errors) {
+ console.error('GraphQL Error:', JSON.stringify(result.errors, null, 2));
+ throw new Error(result.errors[0].message);
+ }
+ return result.data;
+}
+
+async function runTest() {
+ console.log('๐งช TESTING FRONTEND CHECKOUT FLOW\n');
+ console.log('=' .repeat(50));
+
+ let checkoutId = null;
+ let checkoutToken = null;
+ let shippingMethodId = null;
+
+ try {
+ // STEP 1: Create checkout (like frontend does on first cart add)
+ console.log('\n๐ฆ STEP 1: Create Checkout');
+ console.log('-'.repeat(50));
+
+ const createResult = await saleorFetch(`
+ mutation CheckoutCreate($input: CheckoutCreateInput!) {
+ checkoutCreate(input: $input) {
+ checkout {
+ id
+ token
+ totalPrice { gross { amount currency } }
+ subtotalPrice { gross { amount } }
+ }
+ errors { field message }
+ }
+ }
+ `, {
+ input: {
+ channel: "default-channel",
+ email: "test@test.com",
+ lines: [],
+ languageCode: "SR"
+ }
+ });
+
+ checkoutId = createResult.checkoutCreate.checkout.id;
+ checkoutToken = createResult.checkoutCreate.checkout.token;
+
+ console.log('โ
Checkout created');
+ console.log(' ID:', checkoutId);
+ console.log(' Token:', checkoutToken);
+ console.log(' Initial Total:', createResult.checkoutCreate.checkout.totalPrice.gross.amount, 'RSD');
+
+ // STEP 2: Add product (like frontend does)
+ console.log('\n๐ STEP 2: Add Product to Cart');
+ console.log('-'.repeat(50));
+
+ // Get a valid variant first
+ const productsResult = await saleorFetch(`
+ query {
+ products(channel: "default-channel", first: 1) {
+ edges {
+ node {
+ variants { id name }
+ }
+ }
+ }
+ }
+ `);
+
+ const variantId = productsResult.products.edges[0].node.variants[0].id;
+
+ const addLineResult = await saleorFetch(`
+ mutation CheckoutLinesAdd($checkoutId: ID!, $lines: [CheckoutLineInput!]!) {
+ checkoutLinesAdd(checkoutId: $checkoutId, lines: $lines) {
+ checkout {
+ id
+ token
+ totalPrice { gross { amount currency } }
+ subtotalPrice { gross { amount } }
+ }
+ errors { field message }
+ }
+ }
+ `, {
+ checkoutId: checkoutId,
+ lines: [{ variantId: variantId, quantity: 1 }]
+ });
+
+ const afterAdd = addLineResult.checkoutLinesAdd.checkout;
+ console.log('โ
Product added');
+ console.log(' Product Total:', afterAdd.totalPrice.gross.amount, 'RSD');
+ console.log(' Subtotal:', afterAdd.subtotalPrice.gross.amount, 'RSD');
+
+ // STEP 3: Refresh checkout by token (what refreshCheckout() does)
+ console.log('\n๐ STEP 3: Refresh Checkout by Token');
+ console.log('-'.repeat(50));
+ console.log(' (This simulates what refreshCheckout() does in the store)');
+
+ const refreshResult = await saleorFetch(`
+ query GetCheckout($token: UUID!) {
+ checkout(token: $token) {
+ id
+ token
+ totalPrice { gross { amount currency } }
+ subtotalPrice { gross { amount } }
+ }
+ }
+ `, { token: checkoutToken });
+
+ console.log('โ
Refreshed checkout');
+ console.log(' Total from refresh:', refreshResult.checkout.totalPrice.gross.amount, 'RSD');
+
+ // STEP 4: Set shipping address
+ console.log('\n๐ STEP 4: Set Shipping Address');
+ console.log('-'.repeat(50));
+
+ const addressResult = await saleorFetch(`
+ mutation CheckoutShippingAddressUpdate($checkoutId: ID!, $shippingAddress: AddressInput!) {
+ checkoutShippingAddressUpdate(checkoutId: $checkoutId, shippingAddress: $shippingAddress) {
+ checkout {
+ id
+ shippingMethods { id name price { amount currency } }
+ }
+ errors { field message }
+ }
+ }
+ `, {
+ checkoutId: checkoutId,
+ shippingAddress: {
+ firstName: "Test",
+ lastName: "User",
+ streetAddress1: "123 Test Street",
+ city: "Belgrade",
+ postalCode: "11000",
+ country: "RS",
+ phone: "+38160123456"
+ }
+ });
+
+ const methods = addressResult.checkoutShippingAddressUpdate.checkout.shippingMethods;
+ console.log('โ
Address set');
+ console.log(' Available shipping methods:', methods.length);
+
+ if (methods.length === 0) {
+ console.log('โ No shipping methods available!');
+ return;
+ }
+
+ methods.forEach((m, i) => {
+ console.log(` [${i+1}] ${m.name}: ${m.price.amount} ${m.price.currency}`);
+ });
+
+ shippingMethodId = methods[0].id;
+ const shippingPrice = methods[0].price.amount;
+
+ // STEP 5: Select shipping method (what happens when user clicks radio button)
+ console.log('\n๐ STEP 5: Select Shipping Method');
+ console.log('-'.repeat(50));
+ console.log(` Selecting: ${methods[0].name} (${shippingPrice} RSD)`);
+
+ const methodResult = await saleorFetch(`
+ mutation CheckoutShippingMethodUpdate($checkoutId: ID!, $shippingMethodId: ID!) {
+ checkoutShippingMethodUpdate(checkoutId: $checkoutId, shippingMethodId: $shippingMethodId) {
+ checkout {
+ id
+ totalPrice { gross { amount currency } }
+ subtotalPrice { gross { amount } }
+ shippingPrice { gross { amount } }
+ }
+ errors { field message }
+ }
+ }
+ `, {
+ checkoutId: checkoutId,
+ shippingMethodId: shippingMethodId
+ });
+
+ const afterMethod = methodResult.checkoutShippingMethodUpdate.checkout;
+ console.log('โ
Shipping method set');
+ console.log(' Total:', afterMethod.totalPrice.gross.amount, 'RSD');
+ console.log(' Subtotal:', afterMethod.subtotalPrice.gross.amount, 'RSD');
+ console.log(' Shipping:', afterMethod.shippingPrice.gross.amount, 'RSD');
+
+ // STEP 6: Refresh checkout again (what refreshCheckout() does after setting method)
+ console.log('\n๐ STEP 6: Refresh Checkout Again');
+ console.log('-'.repeat(50));
+ console.log(' (Simulating refreshCheckout() call in handleShippingMethodSelect)');
+
+ const finalRefresh = await saleorFetch(`
+ query GetCheckout($token: UUID!) {
+ checkout(token: $token) {
+ id
+ token
+ totalPrice { gross { amount currency } }
+ subtotalPrice { gross { amount } }
+ shippingPrice { gross { amount } }
+ }
+ }
+ `, { token: checkoutToken });
+
+ const final = finalRefresh.checkout;
+ console.log('โ
Final checkout state after refresh:');
+ console.log(' Total:', final.totalPrice.gross.amount, 'RSD');
+ console.log(' Subtotal:', final.subtotalPrice.gross.amount, 'RSD');
+ console.log(' Shipping:', final.shippingPrice.gross.amount, 'RSD');
+
+ // VERIFICATION
+ console.log('\n๐ VERIFICATION');
+ console.log('=' .repeat(50));
+ const expectedTotal = final.subtotalPrice.gross.amount + final.shippingPrice.gross.amount;
+ const actualTotal = final.totalPrice.gross.amount;
+
+ if (actualTotal === expectedTotal) {
+ console.log('โ
PASS: API returns correct total with shipping');
+ console.log(` ${final.subtotalPrice.gross.amount} + ${final.shippingPrice.gross.amount} = ${actualTotal}`);
+ } else {
+ console.log('โ FAIL: API total does not include shipping');
+ console.log(` Expected: ${expectedTotal}, Got: ${actualTotal}`);
+ }
+
+ console.log('\n๐ FRONTEND ISSUE ANALYSIS');
+ console.log('=' .repeat(50));
+ console.log('The API works correctly. The bug is in the frontend.');
+ console.log('');
+ console.log('What should happen:');
+ console.log(' 1. User selects shipping method โ handleShippingMethodSelect()');
+ console.log(' 2. Calls checkoutService.updateShippingMethod() โ API updates');
+ console.log(' 3. Calls refreshCheckout() โ store updates with new checkout');
+ console.log(' 4. Component re-renders with new checkout.totalPrice');
+ console.log('');
+ console.log('Check browser console for:');
+ console.log(' - [Checkout Debug] logs showing totalPrice values');
+ console.log(' - Network tab showing the GraphQL mutation/refresh calls');
+ console.log(' - React DevTools showing if checkout object updates');
+
+ } catch (error) {
+ console.error('\nโ Test failed:', error.message);
+ process.exit(1);
+ }
+}
+
+runTest();
diff --git a/scripts/test-order-creation.js b/scripts/test-order-creation.js
new file mode 100644
index 0000000..3f5e6a8
--- /dev/null
+++ b/scripts/test-order-creation.js
@@ -0,0 +1,232 @@
+#!/usr/bin/env node
+/**
+ * Full order creation test via API
+ * Tests complete checkout flow including order completion
+ */
+
+const SALEOR_API_URL = 'https://api.manoonoils.com/graphql/';
+
+async function saleorFetch(query, variables = {}) {
+ const response = await fetch(SALEOR_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query: query.replace(/\n\s*/g, ' '), variables }),
+ });
+ const result = await response.json();
+ if (result.errors) {
+ console.error('GraphQL Error:', JSON.stringify(result.errors, null, 2));
+ throw new Error(result.errors[0].message);
+ }
+ return result.data;
+}
+
+async function runOrderTest() {
+ console.log('๐งช FULL ORDER CREATION TEST ON DEV BRANCH\n');
+ console.log('=' .repeat(60));
+
+ try {
+ // STEP 1: Create checkout
+ console.log('\n๐ฆ STEP 1: Create Checkout');
+ const createResult = await saleorFetch(`
+ mutation CheckoutCreate($input: CheckoutCreateInput!) {
+ checkoutCreate(input: $input) {
+ checkout {
+ id
+ token
+ totalPrice { gross { amount currency } }
+ }
+ errors { field message }
+ }
+ }
+ `, {
+ input: {
+ channel: "default-channel",
+ email: "test-order@example.com",
+ lines: [],
+ languageCode: "SR"
+ }
+ });
+
+ const checkoutId = createResult.checkoutCreate.checkout.id;
+ console.log('โ
Checkout created:', checkoutId);
+
+ // STEP 2: Get product and add to cart
+ console.log('\n๐ STEP 2: Add Product');
+ const productsResult = await saleorFetch(`
+ query {
+ products(channel: "default-channel", first: 1) {
+ edges { node { variants { id name } } }
+ }
+ }
+ `);
+ const variantId = productsResult.products.edges[0].node.variants[0].id;
+
+ await saleorFetch(`
+ mutation CheckoutLinesAdd($checkoutId: ID!, $lines: [CheckoutLineInput!]!) {
+ checkoutLinesAdd(checkoutId: $checkoutId, lines: $lines) {
+ checkout { id }
+ errors { field message }
+ }
+ }
+ `, {
+ checkoutId: checkoutId,
+ lines: [{ variantId: variantId, quantity: 1 }]
+ });
+ console.log('โ
Product added');
+
+ // STEP 3: Update email
+ console.log('\n๐ง STEP 3: Update Email');
+ await saleorFetch(`
+ mutation CheckoutEmailUpdate($checkoutId: ID!, $email: String!) {
+ checkoutEmailUpdate(checkoutId: $checkoutId, email: $email) {
+ checkout { id }
+ errors { field message }
+ }
+ }
+ `, { checkoutId: checkoutId, email: "test-order@example.com" });
+ console.log('โ
Email updated');
+
+ // STEP 4: Set shipping address
+ console.log('\n๐ STEP 4: Set Shipping Address');
+ await saleorFetch(`
+ mutation CheckoutShippingAddressUpdate($checkoutId: ID!, $shippingAddress: AddressInput!) {
+ checkoutShippingAddressUpdate(checkoutId: $checkoutId, shippingAddress: $shippingAddress) {
+ checkout {
+ id
+ shippingMethods { id name price { amount } }
+ }
+ errors { field message }
+ }
+ }
+ `, {
+ checkoutId: checkoutId,
+ shippingAddress: {
+ firstName: "Test",
+ lastName: "User",
+ streetAddress1: "123 Test Street",
+ city: "Belgrade",
+ postalCode: "11000",
+ country: "RS",
+ phone: "+38160123456"
+ }
+ });
+
+ // Get shipping methods
+ const methodsResult = await saleorFetch(`
+ query GetCheckout($token: UUID!) {
+ checkout(token: $token) {
+ shippingMethods { id name price { amount } }
+ }
+ }
+ `, { token: createResult.checkoutCreate.checkout.token });
+
+ const shippingMethodId = methodsResult.checkout.shippingMethods[0].id;
+ console.log('โ
Address set, shipping method available:', methodsResult.checkout.shippingMethods[0].name);
+
+ // STEP 5: Set billing address
+ console.log('\n๐ณ STEP 5: Set Billing Address');
+ await saleorFetch(`
+ mutation CheckoutBillingAddressUpdate($checkoutId: ID!, $billingAddress: AddressInput!) {
+ checkoutBillingAddressUpdate(checkoutId: $checkoutId, billingAddress: $billingAddress) {
+ checkout { id }
+ errors { field message }
+ }
+ }
+ `, {
+ checkoutId: checkoutId,
+ billingAddress: {
+ firstName: "Test",
+ lastName: "User",
+ streetAddress1: "123 Test Street",
+ city: "Belgrade",
+ postalCode: "11000",
+ country: "RS",
+ phone: "+38160123456"
+ }
+ });
+ console.log('โ
Billing address set');
+
+ // STEP 6: Select shipping method
+ console.log('\n๐ STEP 6: Select Shipping Method');
+ await saleorFetch(`
+ mutation CheckoutShippingMethodUpdate($checkoutId: ID!, $shippingMethodId: ID!) {
+ checkoutShippingMethodUpdate(checkoutId: $checkoutId, shippingMethodId: $shippingMethodId) {
+ checkout {
+ id
+ totalPrice { gross { amount } }
+ subtotalPrice { gross { amount } }
+ shippingPrice { gross { amount } }
+ }
+ errors { field message }
+ }
+ }
+ `, { checkoutId: checkoutId, shippingMethodId: shippingMethodId });
+ console.log('โ
Shipping method selected');
+
+ // STEP 7: Complete checkout (create order)
+ console.log('\nโ
STEP 7: Complete Checkout (Create Order)');
+ console.log('-'.repeat(60));
+
+ const completeResult = await saleorFetch(`
+ mutation CheckoutComplete($checkoutId: ID!) {
+ checkoutComplete(checkoutId: $checkoutId) {
+ order {
+ id
+ number
+ status
+ created
+ total {
+ gross { amount currency }
+ }
+ subtotal {
+ gross { amount }
+ }
+ shippingPrice {
+ gross { amount }
+ }
+ }
+ errors { field message }
+ }
+ }
+ `, { checkoutId: checkoutId });
+
+ if (completeResult.checkoutComplete.errors?.length > 0) {
+ throw new Error(`Order creation failed: ${completeResult.checkoutComplete.errors[0].message}`);
+ }
+
+ const order = completeResult.checkoutComplete.order;
+
+ console.log('โ
ORDER CREATED SUCCESSFULLY!');
+ console.log('');
+ console.log('Order Details:');
+ console.log(' Order ID:', order.id);
+ console.log(' Order Number:', order.number);
+ console.log(' Status:', order.status);
+ console.log(' Created:', order.created);
+ console.log('');
+ console.log('Pricing:');
+ console.log(' Subtotal:', order.subtotal.gross.amount, 'RSD');
+ console.log(' Shipping:', order.shippingPrice.gross.amount, 'RSD');
+ console.log(' Total:', order.total.gross.amount, 'RSD');
+
+ // Verification
+ const expectedTotal = order.subtotal.gross.amount + order.shippingPrice.gross.amount;
+ console.log('');
+ console.log('๐ VERIFICATION:');
+ if (order.total.gross.amount === expectedTotal) {
+ console.log('โ
PASS: Order total includes shipping correctly');
+ console.log(` ${order.subtotal.gross.amount} + ${order.shippingPrice.gross.amount} = ${order.total.gross.amount}`);
+ } else {
+ console.log('โ FAIL: Order total does not match expected');
+ }
+
+ console.log('');
+ console.log('๐ DEV BRANCH TEST COMPLETE - ALL SYSTEMS GO!');
+
+ } catch (error) {
+ console.error('\nโ Test failed:', error.message);
+ process.exit(1);
+ }
+}
+
+runOrderTest();
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
index b7c9568..14932f7 100644
--- a/src/app/[locale]/layout.tsx
+++ b/src/app/[locale]/layout.tsx
@@ -3,6 +3,8 @@ import { NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from "@/lib/i18n/locales";
import { OpenPanelComponent } from "@openpanel/nextjs";
+import Script from "next/script";
+import { RYBBIT_HOST, RYBBIT_SITE_ID } from "@/lib/services/RybbitService";
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
@@ -46,13 +48,18 @@ export default async function LocaleLayout({
return (
<>
-
+
+
{children}
diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts
index 3d6d1ea..ead87df 100644
--- a/src/lib/analytics.ts
+++ b/src/lib/analytics.ts
@@ -2,11 +2,119 @@
import { useOpenPanel } from "@openpanel/nextjs";
import { useCallback } from "react";
+import {
+ trackRybbitProductView,
+ trackRybbitAddToCart,
+ trackRybbitRemoveFromCart,
+ trackRybbitCheckoutStarted,
+ trackRybbitCheckoutStep,
+ trackRybbitOrderCompleted,
+ trackRybbitSearch,
+ trackRybbitExternalLink,
+ trackRybbitCartView,
+ trackRybbitWishlistAdd,
+ trackRybbitUserLogin,
+ trackRybbitUserRegister,
+} from "@/lib/services/RybbitService";
export function useAnalytics() {
const op = useOpenPanel();
- // Client-side tracking for user behavior
+ // Helper to track with both OpenPanel and Rybbit
+ const trackDual = useCallback((
+ eventName: string,
+ openPanelData: Record
+ ) => {
+ // OpenPanel tracking
+ try {
+ op.track(eventName, openPanelData);
+ } catch (e) {
+ console.error("[OpenPanel] Tracking error:", e);
+ }
+
+ // Rybbit tracking (fire-and-forget)
+ try {
+ switch (eventName) {
+ case "product_viewed":
+ trackRybbitProductView({
+ id: openPanelData.product_id,
+ name: openPanelData.product_name,
+ price: openPanelData.price,
+ currency: openPanelData.currency,
+ category: openPanelData.category,
+ });
+ break;
+ case "add_to_cart":
+ trackRybbitAddToCart({
+ id: openPanelData.product_id,
+ name: openPanelData.product_name,
+ price: openPanelData.price,
+ currency: openPanelData.currency,
+ quantity: openPanelData.quantity,
+ variant: openPanelData.variant,
+ });
+ break;
+ case "remove_from_cart":
+ trackRybbitRemoveFromCart({
+ id: openPanelData.product_id,
+ name: openPanelData.product_name,
+ quantity: openPanelData.quantity,
+ });
+ break;
+ case "cart_view":
+ trackRybbitCartView({
+ total: openPanelData.cart_total,
+ currency: openPanelData.currency,
+ item_count: openPanelData.item_count,
+ });
+ break;
+ case "checkout_started":
+ trackRybbitCheckoutStarted({
+ total: openPanelData.cart_total,
+ currency: openPanelData.currency,
+ item_count: openPanelData.item_count,
+ items: openPanelData.items,
+ });
+ break;
+ case "checkout_step":
+ trackRybbitCheckoutStep(openPanelData.step, openPanelData);
+ break;
+ case "order_completed":
+ trackRybbitOrderCompleted({
+ order_id: openPanelData.order_id,
+ order_number: openPanelData.order_number,
+ total: openPanelData.total,
+ currency: openPanelData.currency,
+ item_count: openPanelData.item_count,
+ shipping_cost: openPanelData.shipping_cost,
+ customer_email: openPanelData.customer_email,
+ payment_method: openPanelData.payment_method,
+ });
+ break;
+ case "search":
+ trackRybbitSearch(openPanelData.query, openPanelData.results_count);
+ break;
+ case "external_link_click":
+ trackRybbitExternalLink(openPanelData.url, openPanelData.label);
+ break;
+ case "wishlist_add":
+ trackRybbitWishlistAdd({
+ id: openPanelData.product_id,
+ name: openPanelData.product_name,
+ });
+ break;
+ case "user_login":
+ trackRybbitUserLogin(openPanelData.method);
+ break;
+ case "user_register":
+ trackRybbitUserRegister(openPanelData.method);
+ break;
+ }
+ } catch (e) {
+ console.warn("[Rybbit] Tracking error:", e);
+ }
+ }, [op]);
+
const trackProductView = useCallback((product: {
id: string;
name: string;
@@ -14,19 +122,15 @@ export function useAnalytics() {
currency: string;
category?: string;
}) => {
- try {
- op.track("product_viewed", {
- product_id: product.id,
- product_name: product.name,
- price: product.price,
- currency: product.currency,
- category: product.category,
- source: "client",
- });
- } catch (e) {
- console.error("[Client Analytics] Product view error:", e);
- }
- }, [op]);
+ trackDual("product_viewed", {
+ product_id: product.id,
+ product_name: product.name,
+ price: product.price,
+ currency: product.currency,
+ category: product.category,
+ source: "client",
+ });
+ }, [trackDual]);
const trackAddToCart = useCallback((product: {
id: string;
@@ -36,37 +140,42 @@ export function useAnalytics() {
quantity: number;
variant?: string;
}) => {
- try {
- op.track("add_to_cart", {
- product_id: product.id,
- product_name: product.name,
- price: product.price,
- currency: product.currency,
- quantity: product.quantity,
- variant: product.variant,
- source: "client",
- });
- } catch (e) {
- console.error("[Client Analytics] Add to cart error:", e);
- }
- }, [op]);
+ trackDual("add_to_cart", {
+ product_id: product.id,
+ product_name: product.name,
+ price: product.price,
+ currency: product.currency,
+ quantity: product.quantity,
+ variant: product.variant,
+ source: "client",
+ });
+ }, [trackDual]);
const trackRemoveFromCart = useCallback((product: {
id: string;
name: string;
quantity: number;
}) => {
- try {
- op.track("remove_from_cart", {
- product_id: product.id,
- product_name: product.name,
- quantity: product.quantity,
- source: "client",
- });
- } catch (e) {
- console.error("[Client Analytics] Remove from cart error:", e);
- }
- }, [op]);
+ trackDual("remove_from_cart", {
+ product_id: product.id,
+ product_name: product.name,
+ quantity: product.quantity,
+ source: "client",
+ });
+ }, [trackDual]);
+
+ const trackCartView = useCallback((cart: {
+ total: number;
+ currency: string;
+ item_count: number;
+ }) => {
+ trackDual("cart_view", {
+ cart_total: cart.total,
+ currency: cart.currency,
+ item_count: cart.item_count,
+ source: "client",
+ });
+ }, [trackDual]);
const trackCheckoutStarted = useCallback((cart: {
total: number;
@@ -79,36 +188,23 @@ export function useAnalytics() {
price: number;
}>;
}) => {
- try {
- op.track("checkout_started", {
- cart_total: cart.total,
- currency: cart.currency,
- item_count: cart.item_count,
- items: cart.items,
- source: "client",
- });
- } catch (e) {
- console.error("[Client Analytics] Checkout started error:", e);
- }
- }, [op]);
+ trackDual("checkout_started", {
+ cart_total: cart.total,
+ currency: cart.currency,
+ item_count: cart.item_count,
+ items: cart.items,
+ source: "client",
+ });
+ }, [trackDual]);
const trackCheckoutStep = useCallback((step: string, data?: Record) => {
- try {
- op.track("checkout_step", {
- step,
- ...data,
- source: "client",
- });
- } catch (e) {
- console.error("[Client Analytics] Checkout step error:", e);
- }
- }, [op]);
+ trackDual("checkout_step", {
+ step,
+ ...data,
+ source: "client",
+ });
+ }, [trackDual]);
- /**
- * DUAL TRACKING: Order completion
- * 1. Track client-side (immediate, captures user session)
- * 2. Call server-side API (reliable, can't be blocked)
- */
const trackOrderCompleted = useCallback(async (order: {
order_id: string;
order_number: string;
@@ -119,37 +215,34 @@ export function useAnalytics() {
customer_email?: string;
payment_method?: string;
}) => {
- console.log("[Dual Analytics] Tracking order:", order.order_number, "Total:", order.total);
+ console.log("[Analytics] Tracking order:", order.order_number);
- // CLIENT-SIDE: Track immediately for user session data
+ // Track with both OpenPanel and Rybbit
+ trackDual("order_completed", {
+ order_id: order.order_id,
+ order_number: order.order_number,
+ total: order.total,
+ currency: order.currency,
+ item_count: order.item_count,
+ shipping_cost: order.shipping_cost,
+ customer_email: order.customer_email,
+ payment_method: order.payment_method,
+ source: "client",
+ });
+
+ // OpenPanel revenue tracking
try {
- op.track("order_completed", {
- order_id: order.order_id,
- order_number: order.order_number,
- total: order.total,
- currency: order.currency,
- item_count: order.item_count,
- shipping_cost: order.shipping_cost,
- customer_email: order.customer_email,
- payment_method: order.payment_method,
- source: "client",
- });
-
op.revenue(order.total, {
currency: order.currency,
transaction_id: order.order_number,
source: "client",
});
-
- console.log("[Client Analytics] Order tracked");
} catch (e) {
- console.error("[Client Analytics] Order tracking error:", e);
+ console.error("[OpenPanel] Revenue tracking error:", e);
}
- // SERVER-SIDE: Call API for reliable tracking
+ // Server-side tracking for reliability
try {
- console.log("[Server Analytics] Calling server-side tracking API...");
-
const response = await fetch("/api/analytics/track-order", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -165,39 +258,54 @@ export function useAnalytics() {
}),
});
- if (response.ok) {
- console.log("[Server Analytics] Order tracked successfully");
- } else {
+ if (!response.ok) {
console.error("[Server Analytics] Failed:", await response.text());
}
} catch (e) {
console.error("[Server Analytics] API call failed:", e);
}
- }, [op]);
+ }, [op, trackDual]);
const trackSearch = useCallback((query: string, results_count: number) => {
- try {
- op.track("search", {
- query,
- results_count,
- source: "client",
- });
- } catch (e) {
- console.error("[Client Analytics] Search error:", e);
- }
- }, [op]);
+ trackDual("search", {
+ query,
+ results_count,
+ source: "client",
+ });
+ }, [trackDual]);
const trackExternalLink = useCallback((url: string, label?: string) => {
- try {
- op.track("external_link_click", {
- url,
- label,
- source: "client",
- });
- } catch (e) {
- console.error("[Client Analytics] External link error:", e);
- }
- }, [op]);
+ trackDual("external_link_click", {
+ url,
+ label,
+ source: "client",
+ });
+ }, [trackDual]);
+
+ const trackWishlistAdd = useCallback((product: {
+ id: string;
+ name: string;
+ }) => {
+ trackDual("wishlist_add", {
+ product_id: product.id,
+ product_name: product.name,
+ source: "client",
+ });
+ }, [trackDual]);
+
+ const trackUserLogin = useCallback((method: string) => {
+ trackDual("user_login", {
+ method,
+ source: "client",
+ });
+ }, [trackDual]);
+
+ const trackUserRegister = useCallback((method: string) => {
+ trackDual("user_register", {
+ method,
+ source: "client",
+ });
+ }, [trackDual]);
const identifyUser = useCallback((user: {
profileId: string;
@@ -213,7 +321,7 @@ export function useAnalytics() {
email: user.email,
});
} catch (e) {
- console.error("[Client Analytics] Identify error:", e);
+ console.error("[OpenPanel] Identify error:", e);
}
}, [op]);
@@ -221,11 +329,15 @@ export function useAnalytics() {
trackProductView,
trackAddToCart,
trackRemoveFromCart,
+ trackCartView,
trackCheckoutStarted,
trackCheckoutStep,
trackOrderCompleted,
trackSearch,
trackExternalLink,
+ trackWishlistAdd,
+ trackUserLogin,
+ trackUserRegister,
identifyUser,
};
}
diff --git a/src/lib/services/RybbitService.ts b/src/lib/services/RybbitService.ts
new file mode 100644
index 0000000..b384e1e
--- /dev/null
+++ b/src/lib/services/RybbitService.ts
@@ -0,0 +1,209 @@
+"use client";
+
+// Rybbit Analytics Service
+// Self-hosted instance at rybbit.nodecrew.me
+
+declare global {
+ interface Window {
+ rybbit?: {
+ event: (eventName: string, eventData?: Record) => void;
+ pageview: () => void;
+ };
+ }
+}
+
+export const RYBBIT_HOST = "https://rybbit.nodecrew.me";
+export const RYBBIT_SITE_ID = "1";
+
+/**
+ * Check if Rybbit is loaded and available
+ */
+export function isRybbitAvailable(): boolean {
+ return typeof window !== "undefined" &&
+ !!window.rybbit &&
+ typeof window.rybbit.event === "function";
+}
+
+/**
+ * Track a custom event with Rybbit
+ */
+export function trackRybbitEvent(
+ eventName: string,
+ eventData?: Record
+): void {
+ if (isRybbitAvailable()) {
+ try {
+ window.rybbit!.event(eventName, eventData);
+ } catch (e) {
+ console.warn("[Rybbit] Event tracking error:", e);
+ }
+ } else {
+ console.warn("[Rybbit] Not available for event:", eventName);
+ }
+}
+
+/**
+ * Track page view manually (usually auto-tracked by Rybbit script)
+ */
+export function trackRybbitPageview(): void {
+ if (isRybbitAvailable()) {
+ try {
+ window.rybbit!.pageview();
+ } catch (e) {
+ console.warn("[Rybbit] Pageview error:", e);
+ }
+ }
+}
+
+// E-commerce Event Tracking Functions
+
+export function trackRybbitProductView(product: {
+ id: string;
+ name: string;
+ price: number;
+ currency: string;
+ category?: string;
+ variant?: string;
+}): void {
+ trackRybbitEvent("product_view", {
+ product_id: product.id,
+ product_name: product.name,
+ price: product.price,
+ currency: product.currency,
+ category: product.category,
+ variant: product.variant,
+ });
+}
+
+export function trackRybbitAddToCart(product: {
+ id: string;
+ name: string;
+ price: number;
+ currency: string;
+ quantity: number;
+ variant?: string;
+}): void {
+ trackRybbitEvent("add_to_cart", {
+ product_id: product.id,
+ product_name: product.name,
+ price: product.price,
+ currency: product.currency,
+ quantity: product.quantity,
+ variant: product.variant,
+ });
+}
+
+export function trackRybbitRemoveFromCart(product: {
+ id: string;
+ name: string;
+ quantity: number;
+}): void {
+ trackRybbitEvent("remove_from_cart", {
+ product_id: product.id,
+ product_name: product.name,
+ quantity: product.quantity,
+ });
+}
+
+export function trackRybbitCartView(cart: {
+ total: number;
+ currency: string;
+ item_count: number;
+}): void {
+ trackRybbitEvent("cart_view", {
+ cart_total: cart.total,
+ currency: cart.currency,
+ item_count: cart.item_count,
+ });
+}
+
+export function trackRybbitCheckoutStarted(cart: {
+ total: number;
+ currency: string;
+ item_count: number;
+ items: Array<{
+ id: string;
+ name: string;
+ quantity: number;
+ price: number;
+ }>;
+}): void {
+ trackRybbitEvent("checkout_started", {
+ cart_total: cart.total,
+ currency: cart.currency,
+ item_count: cart.item_count,
+ items: cart.items,
+ });
+}
+
+export function trackRybbitCheckoutStep(step: string, data?: Record): void {
+ trackRybbitEvent("checkout_step", {
+ step,
+ ...data,
+ });
+}
+
+export function trackRybbitOrderCompleted(order: {
+ order_id: string;
+ order_number: string;
+ total: number;
+ currency: string;
+ item_count: number;
+ shipping_cost?: number;
+ customer_email?: string;
+ payment_method?: string;
+}): void {
+ trackRybbitEvent("order_completed", {
+ order_id: order.order_id,
+ order_number: order.order_number,
+ total: order.total,
+ currency: order.currency,
+ item_count: order.item_count,
+ shipping_cost: order.shipping_cost,
+ customer_email: order.customer_email,
+ payment_method: order.payment_method,
+ });
+}
+
+export function trackRybbitSearch(query: string, results_count: number): void {
+ trackRybbitEvent("search", {
+ query,
+ results_count,
+ });
+}
+
+export function trackRybbitExternalLink(url: string, label?: string): void {
+ trackRybbitEvent("external_link_click", {
+ url,
+ label,
+ });
+}
+
+export function trackRybbitNewsletterSignup(email: string, source: string): void {
+ trackRybbitEvent("newsletter_signup", {
+ email,
+ source,
+ });
+}
+
+export function trackRybbitWishlistAdd(product: {
+ id: string;
+ name: string;
+}): void {
+ trackRybbitEvent("wishlist_add", {
+ product_id: product.id,
+ product_name: product.name,
+ });
+}
+
+export function trackRybbitUserLogin(method: string): void {
+ trackRybbitEvent("user_login", {
+ method,
+ });
+}
+
+export function trackRybbitUserRegister(method: string): void {
+ trackRybbitEvent("user_register", {
+ method,
+ });
+}