feat: integrate Rybbit analytics alongside OpenPanel
Some checks failed
Build and Deploy / build (push) Has been cancelled
Some checks failed
Build and Deploy / build (push) Has been cancelled
- Add RybbitService for tracking e-commerce events - Update useAnalytics hook to track with both OpenPanel and Rybbit - Add Rybbit script to layout for page view tracking - Track all applicable store events: product views, cart, checkout, orders, search, etc.
This commit is contained in:
3
features.md
Normal file
3
features.md
Normal file
@@ -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
|
||||||
40
public/debug-op.js
Normal file
40
public/debug-op.js
Normal file
@@ -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)');
|
||||||
|
})();
|
||||||
310
scripts/test-checkout-shipping.js
Normal file
310
scripts/test-checkout-shipping.js
Normal file
@@ -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();
|
||||||
0
scripts/test-frontend-checkout.js
Normal file
0
scripts/test-frontend-checkout.js
Normal file
137
scripts/test-frontend.mjs
Normal file
137
scripts/test-frontend.mjs
Normal file
@@ -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);
|
||||||
254
scripts/test-full-checkout-flow.js
Normal file
254
scripts/test-full-checkout-flow.js
Normal file
@@ -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();
|
||||||
232
scripts/test-order-creation.js
Normal file
232
scripts/test-order-creation.js
Normal file
@@ -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();
|
||||||
@@ -3,6 +3,8 @@ import { NextIntlClientProvider } from "next-intl";
|
|||||||
import { getMessages, setRequestLocale } from "next-intl/server";
|
import { getMessages, setRequestLocale } from "next-intl/server";
|
||||||
import { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from "@/lib/i18n/locales";
|
import { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from "@/lib/i18n/locales";
|
||||||
import { OpenPanelComponent } from "@openpanel/nextjs";
|
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";
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
|
||||||
|
|
||||||
@@ -46,13 +48,18 @@ export default async function LocaleLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OpenPanelComponent
|
<OpenPanelComponent
|
||||||
clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || ""}
|
clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || ""}
|
||||||
trackScreenViews={true}
|
trackScreenViews={true}
|
||||||
trackOutgoingLinks={true}
|
trackOutgoingLinks={true}
|
||||||
apiUrl="https://op.nodecrew.me/api"
|
apiUrl="https://op.nodecrew.me/api"
|
||||||
scriptUrl="https://op.nodecrew.me/op1.js"
|
scriptUrl="https://op.nodecrew.me/op1.js"
|
||||||
/>
|
/>
|
||||||
|
<Script
|
||||||
|
src={`${RYBBIT_HOST}/api/script.js`}
|
||||||
|
data-site-id={RYBBIT_SITE_ID}
|
||||||
|
strategy="afterInteractive"
|
||||||
|
/>
|
||||||
<NextIntlClientProvider messages={messages}>
|
<NextIntlClientProvider messages={messages}>
|
||||||
{children}
|
{children}
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
|
|||||||
@@ -2,11 +2,119 @@
|
|||||||
|
|
||||||
import { useOpenPanel } from "@openpanel/nextjs";
|
import { useOpenPanel } from "@openpanel/nextjs";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import {
|
||||||
|
trackRybbitProductView,
|
||||||
|
trackRybbitAddToCart,
|
||||||
|
trackRybbitRemoveFromCart,
|
||||||
|
trackRybbitCheckoutStarted,
|
||||||
|
trackRybbitCheckoutStep,
|
||||||
|
trackRybbitOrderCompleted,
|
||||||
|
trackRybbitSearch,
|
||||||
|
trackRybbitExternalLink,
|
||||||
|
trackRybbitCartView,
|
||||||
|
trackRybbitWishlistAdd,
|
||||||
|
trackRybbitUserLogin,
|
||||||
|
trackRybbitUserRegister,
|
||||||
|
} from "@/lib/services/RybbitService";
|
||||||
|
|
||||||
export function useAnalytics() {
|
export function useAnalytics() {
|
||||||
const op = useOpenPanel();
|
const op = useOpenPanel();
|
||||||
|
|
||||||
// Client-side tracking for user behavior
|
// Helper to track with both OpenPanel and Rybbit
|
||||||
|
const trackDual = useCallback((
|
||||||
|
eventName: string,
|
||||||
|
openPanelData: Record<string, any>
|
||||||
|
) => {
|
||||||
|
// 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: {
|
const trackProductView = useCallback((product: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -14,19 +122,15 @@ export function useAnalytics() {
|
|||||||
currency: string;
|
currency: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
trackDual("product_viewed", {
|
||||||
op.track("product_viewed", {
|
product_id: product.id,
|
||||||
product_id: product.id,
|
product_name: product.name,
|
||||||
product_name: product.name,
|
price: product.price,
|
||||||
price: product.price,
|
currency: product.currency,
|
||||||
currency: product.currency,
|
category: product.category,
|
||||||
category: product.category,
|
source: "client",
|
||||||
source: "client",
|
});
|
||||||
});
|
}, [trackDual]);
|
||||||
} catch (e) {
|
|
||||||
console.error("[Client Analytics] Product view error:", e);
|
|
||||||
}
|
|
||||||
}, [op]);
|
|
||||||
|
|
||||||
const trackAddToCart = useCallback((product: {
|
const trackAddToCart = useCallback((product: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -36,37 +140,42 @@ export function useAnalytics() {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
trackDual("add_to_cart", {
|
||||||
op.track("add_to_cart", {
|
product_id: product.id,
|
||||||
product_id: product.id,
|
product_name: product.name,
|
||||||
product_name: product.name,
|
price: product.price,
|
||||||
price: product.price,
|
currency: product.currency,
|
||||||
currency: product.currency,
|
quantity: product.quantity,
|
||||||
quantity: product.quantity,
|
variant: product.variant,
|
||||||
variant: product.variant,
|
source: "client",
|
||||||
source: "client",
|
});
|
||||||
});
|
}, [trackDual]);
|
||||||
} catch (e) {
|
|
||||||
console.error("[Client Analytics] Add to cart error:", e);
|
|
||||||
}
|
|
||||||
}, [op]);
|
|
||||||
|
|
||||||
const trackRemoveFromCart = useCallback((product: {
|
const trackRemoveFromCart = useCallback((product: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
trackDual("remove_from_cart", {
|
||||||
op.track("remove_from_cart", {
|
product_id: product.id,
|
||||||
product_id: product.id,
|
product_name: product.name,
|
||||||
product_name: product.name,
|
quantity: product.quantity,
|
||||||
quantity: product.quantity,
|
source: "client",
|
||||||
source: "client",
|
});
|
||||||
});
|
}, [trackDual]);
|
||||||
} catch (e) {
|
|
||||||
console.error("[Client Analytics] Remove from cart error:", e);
|
const trackCartView = useCallback((cart: {
|
||||||
}
|
total: number;
|
||||||
}, [op]);
|
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: {
|
const trackCheckoutStarted = useCallback((cart: {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -79,36 +188,23 @@ export function useAnalytics() {
|
|||||||
price: number;
|
price: number;
|
||||||
}>;
|
}>;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
trackDual("checkout_started", {
|
||||||
op.track("checkout_started", {
|
cart_total: cart.total,
|
||||||
cart_total: cart.total,
|
currency: cart.currency,
|
||||||
currency: cart.currency,
|
item_count: cart.item_count,
|
||||||
item_count: cart.item_count,
|
items: cart.items,
|
||||||
items: cart.items,
|
source: "client",
|
||||||
source: "client",
|
});
|
||||||
});
|
}, [trackDual]);
|
||||||
} catch (e) {
|
|
||||||
console.error("[Client Analytics] Checkout started error:", e);
|
|
||||||
}
|
|
||||||
}, [op]);
|
|
||||||
|
|
||||||
const trackCheckoutStep = useCallback((step: string, data?: Record<string, unknown>) => {
|
const trackCheckoutStep = useCallback((step: string, data?: Record<string, unknown>) => {
|
||||||
try {
|
trackDual("checkout_step", {
|
||||||
op.track("checkout_step", {
|
step,
|
||||||
step,
|
...data,
|
||||||
...data,
|
source: "client",
|
||||||
source: "client",
|
});
|
||||||
});
|
}, [trackDual]);
|
||||||
} catch (e) {
|
|
||||||
console.error("[Client Analytics] Checkout step error:", e);
|
|
||||||
}
|
|
||||||
}, [op]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: {
|
const trackOrderCompleted = useCallback(async (order: {
|
||||||
order_id: string;
|
order_id: string;
|
||||||
order_number: string;
|
order_number: string;
|
||||||
@@ -119,37 +215,34 @@ export function useAnalytics() {
|
|||||||
customer_email?: string;
|
customer_email?: string;
|
||||||
payment_method?: 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 {
|
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, {
|
op.revenue(order.total, {
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
transaction_id: order.order_number,
|
transaction_id: order.order_number,
|
||||||
source: "client",
|
source: "client",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[Client Analytics] Order tracked");
|
|
||||||
} catch (e) {
|
} 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 {
|
try {
|
||||||
console.log("[Server Analytics] Calling server-side tracking API...");
|
|
||||||
|
|
||||||
const response = await fetch("/api/analytics/track-order", {
|
const response = await fetch("/api/analytics/track-order", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -165,39 +258,54 @@ export function useAnalytics() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (!response.ok) {
|
||||||
console.log("[Server Analytics] Order tracked successfully");
|
|
||||||
} else {
|
|
||||||
console.error("[Server Analytics] Failed:", await response.text());
|
console.error("[Server Analytics] Failed:", await response.text());
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Server Analytics] API call failed:", e);
|
console.error("[Server Analytics] API call failed:", e);
|
||||||
}
|
}
|
||||||
}, [op]);
|
}, [op, trackDual]);
|
||||||
|
|
||||||
const trackSearch = useCallback((query: string, results_count: number) => {
|
const trackSearch = useCallback((query: string, results_count: number) => {
|
||||||
try {
|
trackDual("search", {
|
||||||
op.track("search", {
|
query,
|
||||||
query,
|
results_count,
|
||||||
results_count,
|
source: "client",
|
||||||
source: "client",
|
});
|
||||||
});
|
}, [trackDual]);
|
||||||
} catch (e) {
|
|
||||||
console.error("[Client Analytics] Search error:", e);
|
|
||||||
}
|
|
||||||
}, [op]);
|
|
||||||
|
|
||||||
const trackExternalLink = useCallback((url: string, label?: string) => {
|
const trackExternalLink = useCallback((url: string, label?: string) => {
|
||||||
try {
|
trackDual("external_link_click", {
|
||||||
op.track("external_link_click", {
|
url,
|
||||||
url,
|
label,
|
||||||
label,
|
source: "client",
|
||||||
source: "client",
|
});
|
||||||
});
|
}, [trackDual]);
|
||||||
} catch (e) {
|
|
||||||
console.error("[Client Analytics] External link error:", e);
|
const trackWishlistAdd = useCallback((product: {
|
||||||
}
|
id: string;
|
||||||
}, [op]);
|
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: {
|
const identifyUser = useCallback((user: {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
@@ -213,7 +321,7 @@ export function useAnalytics() {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Client Analytics] Identify error:", e);
|
console.error("[OpenPanel] Identify error:", e);
|
||||||
}
|
}
|
||||||
}, [op]);
|
}, [op]);
|
||||||
|
|
||||||
@@ -221,11 +329,15 @@ export function useAnalytics() {
|
|||||||
trackProductView,
|
trackProductView,
|
||||||
trackAddToCart,
|
trackAddToCart,
|
||||||
trackRemoveFromCart,
|
trackRemoveFromCart,
|
||||||
|
trackCartView,
|
||||||
trackCheckoutStarted,
|
trackCheckoutStarted,
|
||||||
trackCheckoutStep,
|
trackCheckoutStep,
|
||||||
trackOrderCompleted,
|
trackOrderCompleted,
|
||||||
trackSearch,
|
trackSearch,
|
||||||
trackExternalLink,
|
trackExternalLink,
|
||||||
|
trackWishlistAdd,
|
||||||
|
trackUserLogin,
|
||||||
|
trackUserRegister,
|
||||||
identifyUser,
|
identifyUser,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
209
src/lib/services/RybbitService.ts
Normal file
209
src/lib/services/RybbitService.ts
Normal file
@@ -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<string, any>) => 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<string, any>
|
||||||
|
): 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<string, unknown>): 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user