@tagadapay/plugin-sdk 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,12 @@ export declare const PAYMENT_CONFIGS: Record<string, PaymentConfig>;
18
18
  export declare function getPaymentConfig(environment?: string): PaymentConfig;
19
19
  /**
20
20
  * Get BasisTheory API key for current environment
21
- * Falls back to environment variable if available
21
+ *
22
+ * Behavior:
23
+ * - LOCAL/DEVELOPMENT: Always uses embedded test key (key_test_us_pub_VExdfbFQARn821iqP8zNaq)
24
+ * Environment variables are ignored to prevent accidental production key usage
25
+ *
26
+ * - PRODUCTION: Uses environment variable if set, falls back to embedded production key
27
+ * This allows deployment-time configuration while having safe defaults
22
28
  */
23
29
  export declare function getBasisTheoryApiKey(environment?: string): string;
@@ -15,14 +15,22 @@ export const PAYMENT_CONFIGS = {
15
15
  },
16
16
  development: {
17
17
  basisTheory: {
18
- // TODO: Replace with actual development BasisTheory public API key
18
+ // Embedded test API key for development
19
19
  publicApiKey: 'key_test_us_pub_VExdfbFQARn821iqP8zNaq',
20
20
  environment: 'sandbox',
21
21
  },
22
22
  },
23
23
  local: {
24
24
  basisTheory: {
25
- // Development/testing API key (sandbox)
25
+ // Embedded test API key for local development
26
+ publicApiKey: 'key_test_us_pub_VExdfbFQARn821iqP8zNaq',
27
+ environment: 'sandbox',
28
+ },
29
+ },
30
+ // Default fallback configuration
31
+ default: {
32
+ basisTheory: {
33
+ // Embedded test API key as fallback
26
34
  publicApiKey: 'key_test_us_pub_VExdfbFQARn821iqP8zNaq',
27
35
  environment: 'sandbox',
28
36
  },
@@ -32,21 +40,30 @@ export const PAYMENT_CONFIGS = {
32
40
  * Get payment configuration based on environment
33
41
  */
34
42
  export function getPaymentConfig(environment = 'local') {
35
- return PAYMENT_CONFIGS[environment] || PAYMENT_CONFIGS.local;
43
+ return PAYMENT_CONFIGS[environment] || PAYMENT_CONFIGS.default || PAYMENT_CONFIGS.local;
36
44
  }
37
45
  /**
38
46
  * Get BasisTheory API key for current environment
39
- * Falls back to environment variable if available
47
+ *
48
+ * Behavior:
49
+ * - LOCAL/DEVELOPMENT: Always uses embedded test key (key_test_us_pub_VExdfbFQARn821iqP8zNaq)
50
+ * Environment variables are ignored to prevent accidental production key usage
51
+ *
52
+ * - PRODUCTION: Uses environment variable if set, falls back to embedded production key
53
+ * This allows deployment-time configuration while having safe defaults
40
54
  */
41
55
  export function getBasisTheoryApiKey(environment = 'local') {
42
- // Check environment variable first (allows override)
43
- if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_BASIS_THEORY_PUBLIC_API_KEY) {
44
- return process.env.NEXT_PUBLIC_BASIS_THEORY_PUBLIC_API_KEY;
45
- }
46
- if (typeof process !== 'undefined' && process.env?.VITE_BASIS_THEORY_PUBLIC_API_KEY) {
47
- return process.env.VITE_BASIS_THEORY_PUBLIC_API_KEY;
56
+ // For production environment, allow environment variable override
57
+ if (environment === 'production') {
58
+ if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_BASIS_THEORY_PUBLIC_API_KEY) {
59
+ return process.env.NEXT_PUBLIC_BASIS_THEORY_PUBLIC_API_KEY;
60
+ }
61
+ if (typeof process !== 'undefined' && process.env?.VITE_BASIS_THEORY_PUBLIC_API_KEY) {
62
+ return process.env.VITE_BASIS_THEORY_PUBLIC_API_KEY;
63
+ }
48
64
  }
49
- // Fall back to embedded configuration
65
+ // For local/development, always use embedded test key to prevent accidental production key usage
66
+ // Fall back to embedded configuration based on environment
50
67
  const config = getPaymentConfig(environment);
51
68
  return config.basisTheory.publicApiKey;
52
69
  }
@@ -2,6 +2,7 @@ export interface CheckoutLineItem {
2
2
  externalProductId?: string | null;
3
3
  externalVariantId?: string | null;
4
4
  variantId?: string | null;
5
+ priceId?: string | null;
5
6
  quantity: number;
6
7
  }
7
8
  export interface CheckoutInitParams {
@@ -229,6 +230,21 @@ export interface UseCheckoutResult {
229
230
  success: boolean;
230
231
  error?: any;
231
232
  }>;
233
+ addLineItems: (lineItems: CheckoutLineItem[]) => Promise<{
234
+ success: boolean;
235
+ error?: any;
236
+ }>;
237
+ removeLineItems: (lineItems: {
238
+ variantId: string;
239
+ quantity?: number;
240
+ }[]) => Promise<{
241
+ success: boolean;
242
+ error?: any;
243
+ }>;
244
+ setItemQuantity: (variantId: string, quantity: number, priceId?: string) => Promise<{
245
+ success: boolean;
246
+ error?: any;
247
+ }>;
232
248
  toggleOrderBump: (orderBumpOfferId: string, selected: boolean) => Promise<{
233
249
  success: boolean;
234
250
  error?: any;
@@ -257,6 +273,12 @@ export interface UseCheckoutResult {
257
273
  shippingCountryChanged?: boolean;
258
274
  billingCountryChanged?: boolean;
259
275
  }>;
276
+ previewOrderSummary: (orderBumpOfferIds: string[], orderBumpType?: 'primary' | 'secondary' | 'vip') => Promise<{
277
+ savings: number;
278
+ savingsPct: number;
279
+ currency: string;
280
+ error?: any;
281
+ }>;
260
282
  clear: () => void;
261
283
  }
262
284
  export declare function useCheckout(options?: UseCheckoutOptions): UseCheckoutResult;
@@ -3,7 +3,7 @@ import { useTagadaContext } from '../providers/TagadaProvider';
3
3
  import { getCheckoutToken } from '../utils/urlUtils';
4
4
  import { useCurrency } from '../hooks/useCurrency';
5
5
  export function useCheckout(options = {}) {
6
- const { apiService } = useTagadaContext();
6
+ const { apiService, updateCheckoutDebugData } = useTagadaContext();
7
7
  const { code: currentCurrency } = useCurrency();
8
8
  const [checkout, setCheckout] = useState(null);
9
9
  const [isLoading, setIsLoading] = useState(false);
@@ -21,6 +21,38 @@ export function useCheckout(options = {}) {
21
21
  }
22
22
  };
23
23
  }, []);
24
+ // Update debug data whenever checkout state changes with comprehensive information
25
+ useEffect(() => {
26
+ const debugData = checkout
27
+ ? {
28
+ checkout,
29
+ sessionId: checkout.checkoutSession?.id,
30
+ checkoutToken: currentCheckoutTokenRef.current,
31
+ currency: checkout.summary?.currency,
32
+ totalAmount: checkout.summary?.totalAmount,
33
+ totalAdjustedAmount: checkout.summary?.totalAdjustedAmount,
34
+ promotionAmount: checkout.summary?.totalPromotionAmount,
35
+ itemsCount: checkout.summary?.items?.length || 0,
36
+ orderBumps: checkout.checkoutSession?.sessionLineItems?.filter((item) => item.isOrderBump) || [],
37
+ adjustments: checkout.summary?.adjustments || [],
38
+ isInitialized,
39
+ lastUpdated: new Date().toISOString(),
40
+ }
41
+ : null;
42
+ updateCheckoutDebugData(debugData, error, isLoading);
43
+ if (debugData) {
44
+ console.log('🐛 [useCheckout] Debug data updated for debug drawer', {
45
+ sessionId: debugData.sessionId,
46
+ totalAmount: debugData.totalAmount,
47
+ totalAdjustedAmount: debugData.totalAdjustedAmount,
48
+ promotionAmount: debugData.promotionAmount,
49
+ itemsCount: debugData.itemsCount,
50
+ orderBumpsCount: debugData.orderBumps?.length || 0,
51
+ adjustmentsCount: debugData.adjustments?.length || 0,
52
+ lastUpdated: debugData.lastUpdated,
53
+ });
54
+ }
55
+ }, [checkout, error, isLoading, isInitialized]); // Removed updateCheckoutDebugData from deps to prevent infinite loop
24
56
  const init = useCallback(async (params) => {
25
57
  // Don't allow init if we already have a checkout token
26
58
  if (providedToken) {
@@ -85,6 +117,14 @@ export function useCheckout(options = {}) {
85
117
  setCheckout(response);
86
118
  currentCheckoutTokenRef.current = checkoutToken;
87
119
  setIsInitialized(true);
120
+ console.log('📊 [useCheckout] Checkout data updated', {
121
+ sessionId: response.checkoutSession?.id,
122
+ totalAmount: response.summary?.totalAmount,
123
+ promotionAmount: response.summary?.totalPromotionAmount,
124
+ itemsCount: response.summary?.items?.length || 0,
125
+ orderBumpsCount: response.checkoutSession?.sessionLineItems?.filter((item) => item.isOrderBump)?.length || 0,
126
+ timestamp: new Date().toISOString(),
127
+ });
88
128
  return response;
89
129
  }
90
130
  catch (err) {
@@ -100,7 +140,12 @@ export function useCheckout(options = {}) {
100
140
  if (!currentCheckoutTokenRef.current) {
101
141
  throw new Error('No checkout session to refresh');
102
142
  }
143
+ console.log('🔄 [useCheckout] Refreshing checkout data...', {
144
+ checkoutToken: currentCheckoutTokenRef.current.substring(0, 8) + '...',
145
+ timestamp: new Date().toISOString(),
146
+ });
103
147
  await getCheckout(currentCheckoutTokenRef.current);
148
+ console.log('✅ [useCheckout] Refresh completed, debug data will be updated automatically');
104
149
  }, [getCheckout]);
105
150
  const updateAddress = useCallback(async (data) => {
106
151
  if (!checkout?.checkoutSession.id) {
@@ -213,6 +258,63 @@ export function useCheckout(options = {}) {
213
258
  throw error;
214
259
  }
215
260
  }, [apiService, checkout?.checkoutSession.id, refresh]);
261
+ const addLineItems = useCallback(async (lineItems) => {
262
+ if (!checkout?.checkoutSession.id) {
263
+ throw new Error('No checkout session available');
264
+ }
265
+ try {
266
+ const response = await apiService.fetch(`/api/v1/checkout-sessions/${checkout.checkoutSession.id}/line-items/add`, {
267
+ method: 'POST',
268
+ body: { lineItems },
269
+ });
270
+ if (response.success) {
271
+ await refresh();
272
+ }
273
+ return response;
274
+ }
275
+ catch (err) {
276
+ const error = err instanceof Error ? err : new Error('Failed to add line items');
277
+ throw error;
278
+ }
279
+ }, [apiService, checkout?.checkoutSession.id, refresh]);
280
+ const removeLineItems = useCallback(async (lineItems) => {
281
+ if (!checkout?.checkoutSession.id) {
282
+ throw new Error('No checkout session available');
283
+ }
284
+ try {
285
+ const response = await apiService.fetch(`/api/v1/checkout-sessions/${checkout.checkoutSession.id}/line-items/remove`, {
286
+ method: 'POST',
287
+ body: { lineItems },
288
+ });
289
+ if (response.success) {
290
+ await refresh();
291
+ }
292
+ return response;
293
+ }
294
+ catch (err) {
295
+ const error = err instanceof Error ? err : new Error('Failed to remove line items');
296
+ throw error;
297
+ }
298
+ }, [apiService, checkout?.checkoutSession.id, refresh]);
299
+ const setItemQuantity = useCallback(async (variantId, quantity, priceId) => {
300
+ if (!checkout?.checkoutSession.id) {
301
+ throw new Error('No checkout session available');
302
+ }
303
+ try {
304
+ const response = await apiService.fetch(`/api/v1/checkout-sessions/${checkout.checkoutSession.id}/line-items/set-quantity`, {
305
+ method: 'POST',
306
+ body: { variantId, quantity, priceId },
307
+ });
308
+ if (response.success) {
309
+ await refresh();
310
+ }
311
+ return response;
312
+ }
313
+ catch (err) {
314
+ const error = err instanceof Error ? err : new Error('Failed to set item quantity');
315
+ throw error;
316
+ }
317
+ }, [apiService, checkout?.checkoutSession.id, refresh]);
216
318
  const toggleOrderBump = useCallback(async (orderBumpOfferId, selected) => {
217
319
  if (!checkout?.checkoutSession.id) {
218
320
  throw new Error('No checkout session available');
@@ -223,7 +325,13 @@ export function useCheckout(options = {}) {
223
325
  body: { orderBumpOfferId, selected },
224
326
  });
225
327
  if (response.success) {
328
+ console.log('🎯 [useCheckout] Order bump toggled successfully, refreshing checkout data...', {
329
+ orderBumpOfferId,
330
+ selected,
331
+ timestamp: new Date().toISOString(),
332
+ });
226
333
  await refresh();
334
+ console.log('✅ [useCheckout] Order bump refresh completed, debug drawer should now show updated data');
227
335
  }
228
336
  return response;
229
337
  }
@@ -268,6 +376,30 @@ export function useCheckout(options = {}) {
268
376
  throw error;
269
377
  }
270
378
  }, [apiService, checkout?.checkoutSession.id, refresh]);
379
+ const previewOrderSummary = useCallback(async (orderBumpOfferIds, orderBumpType = 'vip') => {
380
+ if (!checkout?.checkoutSession.id) {
381
+ throw new Error('No checkout session available');
382
+ }
383
+ try {
384
+ const response = await apiService.fetch(`/api/v1/checkout-sessions/${checkout.checkoutSession.id}/vip-preview`, {
385
+ method: 'POST',
386
+ body: {
387
+ orderBumpOfferIds,
388
+ orderBumpType,
389
+ },
390
+ });
391
+ return response;
392
+ }
393
+ catch (err) {
394
+ const error = err instanceof Error ? err : new Error('Failed to preview order summary');
395
+ return {
396
+ savings: 0,
397
+ savingsPct: 0,
398
+ currency: checkout?.summary?.currency ?? 'EUR',
399
+ error: error.message,
400
+ };
401
+ }
402
+ }, [apiService, checkout?.checkoutSession.id, checkout?.summary?.currency]);
271
403
  const clear = useCallback(() => {
272
404
  setCheckout(null);
273
405
  setError(null);
@@ -317,9 +449,13 @@ export function useCheckout(options = {}) {
317
449
  removePromotion,
318
450
  getAppliedPromotions,
319
451
  updateLineItems,
452
+ addLineItems,
453
+ removeLineItems,
454
+ setItemQuantity,
320
455
  toggleOrderBump,
321
456
  updateCustomer,
322
457
  updateCustomerAndSessionInfo,
458
+ previewOrderSummary,
323
459
  clear,
324
460
  };
325
461
  }
@@ -0,0 +1,30 @@
1
+ export interface OrderBumpPreview {
2
+ savings: number;
3
+ savingsPct: number;
4
+ currency: string;
5
+ selectedOffers: {
6
+ productId: string | null;
7
+ variantId: string | null;
8
+ isSelected: boolean;
9
+ }[];
10
+ }
11
+ export interface UseOrderBumpOptions {
12
+ checkoutSessionId?: string;
13
+ offerId: string;
14
+ orderBumpType?: 'primary' | 'secondary' | 'vip';
15
+ autoPreview?: boolean;
16
+ }
17
+ export interface UseOrderBumpResult {
18
+ isSelected: boolean;
19
+ preview: OrderBumpPreview | null;
20
+ savings: number | null;
21
+ isLoading: boolean;
22
+ isToggling: boolean;
23
+ error: Error | null;
24
+ toggle: (selected?: boolean) => Promise<{
25
+ success: boolean;
26
+ error?: any;
27
+ }>;
28
+ refreshPreview: () => Promise<void>;
29
+ }
30
+ export declare function useOrderBump(options: UseOrderBumpOptions): UseOrderBumpResult;
@@ -0,0 +1,97 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import { useTagadaContext } from '../providers/TagadaProvider';
3
+ export function useOrderBump(options) {
4
+ const { apiService } = useTagadaContext();
5
+ const { checkoutSessionId, offerId, orderBumpType = 'vip', autoPreview = true } = options;
6
+ const [isSelected, setIsSelected] = useState(false);
7
+ const [preview, setPreview] = useState(null);
8
+ const [isLoading, setIsLoading] = useState(false);
9
+ const [isToggling, setIsToggling] = useState(false);
10
+ const [error, setError] = useState(null);
11
+ const refreshPreview = useCallback(async () => {
12
+ if (!checkoutSessionId)
13
+ return;
14
+ setIsLoading(true);
15
+ setError(null);
16
+ try {
17
+ const response = await apiService.fetch(`/api/v1/checkout-sessions/${checkoutSessionId}/vip-preview`, {
18
+ method: 'POST',
19
+ body: {
20
+ orderBumpOfferIds: [offerId],
21
+ orderBumpType,
22
+ },
23
+ });
24
+ setPreview(response);
25
+ // Update isSelected based on preview data
26
+ const offerSelected = response.selectedOffers?.some((offer) => offer.isSelected);
27
+ setIsSelected(offerSelected ?? false);
28
+ }
29
+ catch (err) {
30
+ const error = err instanceof Error ? err : new Error('Failed to fetch preview');
31
+ setError(error);
32
+ console.error('Order bump preview failed:', error);
33
+ }
34
+ finally {
35
+ setIsLoading(false);
36
+ }
37
+ }, [checkoutSessionId, offerId, orderBumpType, apiService]);
38
+ const toggle = useCallback(async (selected) => {
39
+ if (!checkoutSessionId) {
40
+ throw new Error('No checkout session available');
41
+ }
42
+ const targetState = selected ?? !isSelected;
43
+ // Optimistic update
44
+ setIsSelected(targetState);
45
+ setIsToggling(true);
46
+ setError(null);
47
+ try {
48
+ const response = await apiService.fetch(`/api/v1/checkout-sessions/${checkoutSessionId}/toggle-order-bump`, {
49
+ method: 'POST',
50
+ body: {
51
+ orderBumpOfferId: offerId,
52
+ selected: targetState,
53
+ },
54
+ });
55
+ if (response.success) {
56
+ // Refresh preview to get updated savings
57
+ await refreshPreview();
58
+ return { success: true };
59
+ }
60
+ else {
61
+ // Revert optimistic update
62
+ setIsSelected(!targetState);
63
+ return { success: false, error: response.error };
64
+ }
65
+ }
66
+ catch (err) {
67
+ // Revert optimistic update
68
+ setIsSelected(!targetState);
69
+ const error = err instanceof Error ? err : new Error('Failed to toggle order bump');
70
+ setError(error);
71
+ return { success: false, error: error.message };
72
+ }
73
+ finally {
74
+ setIsToggling(false);
75
+ }
76
+ }, [checkoutSessionId, offerId, isSelected, apiService, refreshPreview]);
77
+ // Auto-fetch preview on mount and when dependencies change
78
+ useEffect(() => {
79
+ if (autoPreview && checkoutSessionId) {
80
+ refreshPreview().catch((error) => {
81
+ console.error('Auto-preview failed:', error);
82
+ });
83
+ }
84
+ }, [autoPreview, refreshPreview]);
85
+ // Calculate current savings
86
+ const savings = isSelected && preview?.savings ? preview.savings : preview?.savings || null;
87
+ return {
88
+ isSelected,
89
+ preview,
90
+ savings,
91
+ isLoading,
92
+ isToggling,
93
+ error,
94
+ toggle,
95
+ refreshPreview,
96
+ };
97
+ }
@@ -1,4 +1,5 @@
1
- import { useState, useCallback, useEffect, useRef } from 'react';
1
+ import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
2
+ import { useBasisTheory } from '@basis-theory/basis-theory-react';
2
3
  import { useTagadaContext } from '../providers/TagadaProvider';
3
4
  import { getBasisTheoryApiKey } from '../config/payment';
4
5
  import { usePaymentPolling } from './usePaymentPolling';
@@ -23,47 +24,25 @@ export function usePayment() {
23
24
  const { createSession, startChallenge } = useThreeds();
24
25
  // Track challenge in progress to prevent multiple challenges
25
26
  const challengeInProgressRef = useRef(false);
26
- // Initialize BasisTheory dynamically
27
- const [basisTheory, setBasisTheory] = useState(null);
28
- const [isBasisTheoryLoading, setIsBasisTheoryLoading] = useState(true);
27
+ // Stabilize environment value to prevent re-renders
28
+ const currentEnvironment = useMemo(() => environment?.environment || 'local', [environment?.environment]);
29
+ // Get API key from embedded configuration with proper environment detection
30
+ const apiKey = useMemo(() => getBasisTheoryApiKey(currentEnvironment), [currentEnvironment]);
31
+ // Initialize BasisTheory using React wrapper
32
+ const { bt: basisTheory, error: btError } = useBasisTheory(apiKey, {
33
+ elements: false,
34
+ });
35
+ // Handle BasisTheory initialization errors (only log once when state changes)
29
36
  useEffect(() => {
30
- let isMounted = true;
31
- const loadBasisTheory = async () => {
32
- try {
33
- setIsBasisTheoryLoading(true);
34
- // Get API key from embedded configuration (with env override support)
35
- const apiKey = getBasisTheoryApiKey(environment?.environment || 'local');
36
- if (!apiKey) {
37
- console.warn('BasisTheory API key not configured');
38
- if (isMounted) {
39
- setError('BasisTheory API key not configured');
40
- setIsBasisTheoryLoading(false);
41
- }
42
- return;
43
- }
44
- const { BasisTheory } = await import('@basis-theory/basis-theory-js');
45
- if (isMounted) {
46
- const bt = await new BasisTheory().init(apiKey, {
47
- elements: false,
48
- });
49
- setBasisTheory(bt);
50
- setIsBasisTheoryLoading(false);
51
- console.log('✅ BasisTheory initialized successfully');
52
- }
53
- }
54
- catch (err) {
55
- console.error('Failed to load BasisTheory:', err);
56
- if (isMounted) {
57
- setError('Failed to initialize payment processor');
58
- setIsBasisTheoryLoading(false);
59
- }
60
- }
61
- };
62
- void loadBasisTheory();
63
- return () => {
64
- isMounted = false;
65
- };
66
- }, [environment]);
37
+ if (btError) {
38
+ console.error('BasisTheory initialization error:', btError);
39
+ setError('Failed to initialize payment processor: ' + btError.message);
40
+ }
41
+ else if (basisTheory && !error) {
42
+ console.log('✅ BasisTheory initialized successfully');
43
+ setError(null); // Clear any previous errors
44
+ }
45
+ }, [basisTheory, btError]); // Removed error from dependency to prevent loops
67
46
  // Clean up polling when component unmounts
68
47
  useEffect(() => {
69
48
  return () => {
@@ -154,9 +133,6 @@ export function usePayment() {
154
133
  }, [apiService, startChallenge, startPolling]);
155
134
  // Create card payment instrument
156
135
  const createCardPaymentInstrument = useCallback(async (cardData) => {
157
- if (isBasisTheoryLoading) {
158
- throw new Error('Payment processor is still initializing. Please wait...');
159
- }
160
136
  if (!basisTheory) {
161
137
  throw new Error('Payment processor not initialized');
162
138
  }
@@ -195,11 +171,11 @@ export function usePayment() {
195
171
  console.error('Error creating card payment instrument:', error);
196
172
  throw error;
197
173
  }
198
- }, [basisTheory, apiService, isBasisTheoryLoading]);
174
+ }, [basisTheory, apiService]);
199
175
  // Create Apple Pay payment instrument
200
176
  const createApplePayPaymentInstrument = useCallback(async (applePayToken) => {
201
- if (isBasisTheoryLoading) {
202
- throw new Error('Payment processor is still initializing. Please wait...');
177
+ if (!basisTheory) {
178
+ throw new Error('Payment processor not initialized');
203
179
  }
204
180
  if (!applePayToken.id) {
205
181
  throw new Error('Apple Pay token is missing');
@@ -227,7 +203,7 @@ export function usePayment() {
227
203
  console.error('Error creating Apple Pay payment instrument:', error);
228
204
  throw error;
229
205
  }
230
- }, [apiService, isBasisTheoryLoading]);
206
+ }, [basisTheory, apiService]);
231
207
  // Process payment directly with checkout session
232
208
  const processPaymentDirect = useCallback(async (checkoutSessionId, paymentInstrumentId, threedsSessionId, options = {}) => {
233
209
  try {
@@ -349,7 +325,7 @@ export function usePayment() {
349
325
  processPaymentWithInstrument,
350
326
  createCardPaymentInstrument,
351
327
  createApplePayPaymentInstrument,
352
- isLoading: isLoading || isBasisTheoryLoading,
328
+ isLoading: isLoading || !basisTheory, // Indicate loading if BasisTheory is not initialized
353
329
  error,
354
330
  clearError,
355
331
  currentPaymentId,
@@ -7,16 +7,12 @@ export interface PaymentInstrument {
7
7
  maskedCardNumber?: string;
8
8
  expirationMonth?: number;
9
9
  expirationYear?: number;
10
- brand?: string;
11
- bin?: string;
12
- last4?: string;
13
- } | null;
10
+ };
14
11
  }
15
12
  export interface ThreedsSession {
16
13
  id: string;
17
- provider: ThreedsProvider;
18
- status: string;
19
- sessionData: any;
14
+ sessionId: string;
15
+ provider: string;
20
16
  }
21
17
  export interface ThreedsChallenge {
22
18
  sessionId: string;
@@ -33,6 +29,8 @@ export interface ThreedsHook {
33
29
  isLoading: boolean;
34
30
  error: Error | null;
35
31
  }
36
- export declare function useThreeds(options?: {
32
+ interface UseThreedsOptions {
37
33
  defaultProvider?: ThreedsProvider;
38
- }): ThreedsHook;
34
+ }
35
+ export declare function useThreeds(options?: UseThreedsOptions): ThreedsHook;
36
+ export {};
@@ -4,22 +4,17 @@ import { getBasisTheoryApiKey } from '../config/payment';
4
4
  import { useThreedsModal } from './useThreedsModal';
5
5
  export function useThreeds(options = {}) {
6
6
  const { defaultProvider = 'basis_theory' } = options;
7
- const { apiService, environment } = useTagadaContext();
7
+ const { apiService } = useTagadaContext();
8
8
  const [isLoading, setIsLoading] = useState(false);
9
9
  const [error, setError] = useState(null);
10
10
  const { createThreedsModal, closeThreedsModal } = useThreedsModal();
11
11
  const [basisTheory3ds, setBasisTheory3ds] = useState(null);
12
+ const [currentChallenge, setCurrentChallenge] = useState(null);
12
13
  // Dynamically import BasisTheory3ds on the client side only
13
14
  useEffect(() => {
14
15
  let isMounted = true;
15
16
  const loadBasisTheory = async () => {
16
17
  try {
17
- // Get API key from embedded configuration (with env override support)
18
- const apiKey = getBasisTheoryApiKey(environment?.environment || 'local');
19
- if (!apiKey) {
20
- console.warn('BasisTheory API key not configured');
21
- return;
22
- }
23
18
  // Dynamically import the library only on the client side
24
19
  const { BasisTheory3ds } = await import('@basis-theory/web-threeds');
25
20
  if (isMounted) {
@@ -38,20 +33,17 @@ export function useThreeds(options = {}) {
38
33
  return () => {
39
34
  isMounted = false;
40
35
  };
41
- }, [environment]);
36
+ }, []);
42
37
  // Create a 3DS session with BasisTheory
43
38
  const createBasisTheorySession = useCallback(async (paymentInstrument) => {
44
39
  try {
45
40
  if (!basisTheory3ds) {
46
41
  throw new Error('BasisTheory3ds not loaded yet');
47
42
  }
48
- // Get API key from embedded configuration
49
- const apiKey = getBasisTheoryApiKey(environment?.environment || 'local');
50
- if (!apiKey) {
51
- throw new Error('BasisTheory API key not configured');
52
- }
43
+ // Use the same API key approach as the working CMS version
44
+ const apiKey = getBasisTheoryApiKey('production'); // Use production config for now
53
45
  const bt3ds = basisTheory3ds(apiKey);
54
- console.log('Creating BasisTheory 3DS session for payment instrument:', paymentInstrument.id);
46
+ console.log('paymentInstrument paymentInstrument', paymentInstrument?.token);
55
47
  const session = await bt3ds.createSession({
56
48
  tokenId: paymentInstrument.token,
57
49
  });
@@ -70,7 +62,7 @@ export function useThreeds(options = {}) {
70
62
  console.error('Error creating BasisTheory 3DS session:', error);
71
63
  throw error;
72
64
  }
73
- }, [apiService, basisTheory3ds, environment]);
65
+ }, [apiService, basisTheory3ds]);
74
66
  // Generic createSession method that supports multiple providers
75
67
  const createSession = useCallback(async (paymentInstrument, options) => {
76
68
  const provider = options?.provider || defaultProvider;
@@ -96,31 +88,37 @@ export function useThreeds(options = {}) {
96
88
  }
97
89
  }, [defaultProvider, createBasisTheorySession]);
98
90
  // Start a 3DS challenge with BasisTheory
99
- const startBasisTheoryChallenge = useCallback(async (challengeData) => {
91
+ const startBasisTheoryChallenge = useCallback(async (sessionId, acsChallengeUrl, acsTransactionId, threeDSVersion) => {
100
92
  try {
101
93
  if (!basisTheory3ds) {
102
94
  throw new Error('BasisTheory3ds not loaded yet');
103
95
  }
104
- // Get API key from embedded configuration
105
- const apiKey = getBasisTheoryApiKey(environment?.environment || 'local');
96
+ // Use the same API key approach as the working CMS version
97
+ const apiKey = getBasisTheoryApiKey('production'); // Use production config for now
106
98
  if (!apiKey) {
107
- throw new Error('BasisTheory API key is not configured');
99
+ throw new Error('BasisTheory API key is not set');
108
100
  }
109
101
  const modal = createThreedsModal({
110
102
  onClose: () => {
103
+ // Throw error when user closes the modal
111
104
  throw new Error('Authentication was cancelled by the user');
112
105
  },
113
106
  });
114
107
  const bt3ds = basisTheory3ds(apiKey);
115
- console.log('Starting BasisTheory 3DS challenge:', challengeData);
108
+ console.log('bt3ds starting challenge with params', {
109
+ sessionId,
110
+ acsChallengeUrl,
111
+ acsTransactionId,
112
+ threeDSVersion,
113
+ });
116
114
  const challengeCompletion = await bt3ds.startChallenge({
117
- sessionId: challengeData.sessionId,
118
- acsChallengeUrl: challengeData.acsChallengeUrl,
119
- acsTransactionId: challengeData.acsTransactionId,
120
- threeDSVersion: challengeData.threeDSVersion,
115
+ sessionId,
116
+ acsChallengeUrl,
117
+ acsTransactionId,
118
+ threeDSVersion: threeDSVersion,
121
119
  containerId: modal.containerId + '-content',
122
120
  mode: 'iframe',
123
- timeout: 60000 * 3, // 3 minutes
121
+ timeout: 60000 * 3,
124
122
  });
125
123
  closeThreedsModal();
126
124
  return challengeCompletion;
@@ -130,7 +128,7 @@ export function useThreeds(options = {}) {
130
128
  closeThreedsModal();
131
129
  throw error;
132
130
  }
133
- }, [basisTheory3ds, createThreedsModal, closeThreedsModal, environment]);
131
+ }, [basisTheory3ds, createThreedsModal, closeThreedsModal]);
134
132
  // Generic startChallenge method that supports multiple providers
135
133
  const startChallenge = useCallback(async (challengeData, options) => {
136
134
  const provider = options?.provider || defaultProvider;
@@ -138,7 +136,7 @@ export function useThreeds(options = {}) {
138
136
  setError(null);
139
137
  try {
140
138
  if (provider === 'basis_theory') {
141
- return await startBasisTheoryChallenge(challengeData);
139
+ return await startBasisTheoryChallenge(challengeData.sessionId, challengeData.acsChallengeUrl, challengeData.acsTransactionId, challengeData.threeDSVersion);
142
140
  }
143
141
  else {
144
142
  throw new Error(`Unsupported 3DS provider: ${String(provider)}`);
@@ -1,16 +1,13 @@
1
- export interface ThreedsModalOptions {
2
- containerId?: string;
3
- mode?: 'fixed' | 'auto-fit';
4
- onClose?: () => void;
5
- }
6
- export interface ThreedsModalInstance {
7
- containerId: string;
8
- getContentElement: () => HTMLElement | null;
9
- updateModalSize: () => void;
10
- cleanup: () => void;
11
- }
12
- export interface ThreedsModalHook {
13
- createThreedsModal: (options?: ThreedsModalOptions) => ThreedsModalInstance;
1
+ export declare function useThreedsModal(): {
2
+ createThreedsModal: (options?: {
3
+ containerId?: string;
4
+ mode?: "fixed" | "auto-fit";
5
+ onClose?: () => void;
6
+ }) => {
7
+ containerId: string;
8
+ getContentElement: () => HTMLElement | null;
9
+ updateModalSize: () => void;
10
+ cleanup: () => void;
11
+ };
14
12
  closeThreedsModal: (containerId?: string) => void;
15
- }
16
- export declare function useThreedsModal(): ThreedsModalHook;
13
+ };
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { useCallback } from 'react';
2
3
  export function useThreedsModal() {
3
4
  // Close the 3DS modal
@@ -44,9 +45,9 @@ export function useThreedsModal() {
44
45
  zIndex: '9998',
45
46
  display: 'flex',
46
47
  justifyContent: 'center',
47
- alignItems: 'center',
48
+ alignItems: 'center', // Default to center alignment
48
49
  transition: 'opacity 0.3s ease',
49
- opacity: '0',
50
+ opacity: '0', // Start transparent
50
51
  });
51
52
  // Create modal container
52
53
  const container = document.createElement('div');
@@ -57,7 +58,7 @@ export function useThreedsModal() {
57
58
  const baseStyles = {
58
59
  position: 'relative',
59
60
  backgroundColor: 'white',
60
- borderRadius: '12px',
61
+ borderRadius: '12px', // Default border radius
61
62
  boxShadow: '0 10px 25px rgba(0, 0, 0, 0.2)',
62
63
  maxWidth: '100%',
63
64
  overflow: 'hidden',
@@ -69,10 +70,12 @@ export function useThreedsModal() {
69
70
  // Function to apply styles based on screen size and mode
70
71
  const applyResponsiveStyles = () => {
71
72
  isMobile = window.innerWidth < 768;
73
+ // Always ensure backdrop is properly centered
72
74
  backdrop.style.alignItems = 'center';
73
75
  backdrop.style.justifyContent = 'center';
74
76
  if (mode === 'fixed') {
75
77
  if (isMobile) {
78
+ // Full screen modal for mobile
76
79
  Object.assign(container.style, {
77
80
  ...baseStyles,
78
81
  position: 'fixed',
@@ -82,7 +85,7 @@ export function useThreedsModal() {
82
85
  bottom: '0',
83
86
  width: '100%',
84
87
  height: '100%',
85
- borderRadius: '0',
88
+ borderRadius: '0', // No border radius for full screen
86
89
  margin: '0',
87
90
  backgroundColor: 'white',
88
91
  transform: 'scale(0.95)',
@@ -91,6 +94,7 @@ export function useThreedsModal() {
91
94
  });
92
95
  }
93
96
  else {
97
+ // Desktop styles
94
98
  Object.assign(container.style, {
95
99
  ...baseStyles,
96
100
  width: '550px',
@@ -108,6 +112,7 @@ export function useThreedsModal() {
108
112
  else {
109
113
  // auto-fit mode
110
114
  if (isMobile) {
115
+ // Full screen modal for mobile
111
116
  Object.assign(container.style, {
112
117
  ...baseStyles,
113
118
  position: 'fixed',
@@ -117,7 +122,7 @@ export function useThreedsModal() {
117
122
  bottom: '0',
118
123
  width: '100%',
119
124
  height: '100%',
120
- borderRadius: '0',
125
+ borderRadius: '0', // No border radius for full screen
121
126
  margin: '0',
122
127
  backgroundColor: 'white',
123
128
  transform: 'scale(0.95)',
@@ -126,6 +131,7 @@ export function useThreedsModal() {
126
131
  });
127
132
  }
128
133
  else {
134
+ // Desktop styles
129
135
  Object.assign(container.style, {
130
136
  ...baseStyles,
131
137
  width: '550px',
@@ -249,6 +255,7 @@ export function useThreedsModal() {
249
255
  if (!content)
250
256
  return;
251
257
  if (isMobile) {
258
+ // On mobile, we're using full screen so just ensure content scrolls
252
259
  content.style.height = 'calc(100% - 60px)';
253
260
  content.style.overflowY = 'auto';
254
261
  }
@@ -258,10 +265,12 @@ export function useThreedsModal() {
258
265
  const headerHeight = header.offsetHeight;
259
266
  const maxModalHeight = viewportHeight * 0.85;
260
267
  if (contentHeight + headerHeight < maxModalHeight) {
268
+ // Content fits, let it determine the height
261
269
  container.style.height = 'auto';
262
270
  content.style.overflowY = 'visible';
263
271
  }
264
272
  else {
273
+ // Content is too large, cap the height
265
274
  container.style.height = `${maxModalHeight}px`;
266
275
  content.style.height = `${maxModalHeight - headerHeight}px`;
267
276
  content.style.overflowY = 'auto';
@@ -270,27 +279,33 @@ export function useThreedsModal() {
270
279
  };
271
280
  // Handle window resize
272
281
  const handleResize = () => {
282
+ // Store current opacity values before applying new styles
273
283
  const currentOpacity = container.style.opacity;
274
284
  const currentTransform = container.style.transform;
275
285
  applyResponsiveStyles();
276
286
  updateContentStyles();
287
+ // Restore opacity and transform to maintain visibility
277
288
  container.style.opacity = currentOpacity;
278
289
  container.style.transform = currentTransform;
290
+ // For auto-fit mode, also adjust the height
279
291
  if (mode === 'auto-fit') {
280
292
  adjustAutoFitHeight();
281
293
  }
282
294
  };
283
295
  // Set up mutation observer to detect content changes
284
- const contentObserver = new MutationObserver(() => {
296
+ const contentObserver = new MutationObserver((mutations) => {
297
+ // When content changes, adjust the height
285
298
  if (mode === 'auto-fit') {
299
+ // Small delay to ensure content has rendered
286
300
  setTimeout(adjustAutoFitHeight, 50);
287
301
  }
288
302
  });
303
+ // Configure and start the mutation observer
289
304
  contentObserver.observe(content, {
290
- childList: true,
291
- subtree: true,
292
- characterData: true,
293
- attributes: true,
305
+ childList: true, // Watch for changes to child elements
306
+ subtree: true, // Watch the entire subtree
307
+ characterData: true, // Watch for changes to text content
308
+ attributes: true, // Watch for changes to attributes
294
309
  });
295
310
  // For auto-fit mode, set up resize observer for content element
296
311
  let resizeObserver = null;
@@ -311,7 +326,7 @@ export function useThreedsModal() {
311
326
  return {
312
327
  containerId,
313
328
  getContentElement: () => document.getElementById(`${containerId}-content`),
314
- updateModalSize,
329
+ updateModalSize, // Expose function to manually update size
315
330
  cleanup: () => {
316
331
  window.removeEventListener('resize', handleResize);
317
332
  if (resizeObserver) {
@@ -5,6 +5,7 @@ export { TagadaProvider } from './providers/TagadaProvider';
5
5
  export { useCheckout } from './hooks/useCheckout';
6
6
  export { useProducts } from './hooks/useProducts';
7
7
  export { useOffers } from './hooks/useOffers';
8
+ export { useOrderBump } from './hooks/useOrderBump';
8
9
  export { useSession } from './hooks/useSession';
9
10
  export { useCurrency } from './hooks/useCurrency';
10
11
  export { useCustomer } from './hooks/useCustomer';
@@ -19,8 +20,8 @@ export { useThreeds } from './hooks/useThreeds';
19
20
  export { useThreedsModal } from './hooks/useThreedsModal';
20
21
  export type { Customer, Session, AuthState, Locale, Currency, Store, Environment, EnvironmentConfig, Order, OrderItem, OrderSummary, OrderAddress, PickupPoint, } from './types';
21
22
  export type { UseCheckoutOptions, UseCheckoutResult, CheckoutInitParams, CheckoutLineItem, CheckoutSession, CheckoutData, Promotion, } from './hooks/useCheckout';
23
+ export type { UseOrderBumpOptions, UseOrderBumpResult, OrderBumpPreview } from './hooks/useOrderBump';
22
24
  export type { Payment, PollingOptions, PaymentPollingHook } from './hooks/usePaymentPolling';
23
25
  export type { PaymentInstrument, ThreedsSession, ThreedsChallenge, ThreedsOptions, ThreedsHook, ThreedsProvider, } from './hooks/useThreeds';
24
- export type { ThreedsModalOptions, ThreedsModalInstance, ThreedsModalHook } from './hooks/useThreedsModal';
25
26
  export type { ApplePayToken, CardPaymentMethod, PaymentResponse, PaymentOptions, PaymentInstrumentResponse, PaymentHook, } from './hooks/usePayment';
26
27
  export { formatMoney, formatMoneyWithoutSymbol, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits, convertCurrency, formatSimpleMoney, } from './utils/money';
@@ -8,6 +8,7 @@ export { TagadaProvider } from './providers/TagadaProvider';
8
8
  export { useCheckout } from './hooks/useCheckout';
9
9
  export { useProducts } from './hooks/useProducts';
10
10
  export { useOffers } from './hooks/useOffers';
11
+ export { useOrderBump } from './hooks/useOrderBump';
11
12
  export { useSession } from './hooks/useSession';
12
13
  export { useCurrency } from './hooks/useCurrency';
13
14
  export { useCustomer } from './hooks/useCustomer';
@@ -39,17 +39,11 @@ interface TagadaProviderProps {
39
39
  children: ReactNode;
40
40
  environment?: Environment;
41
41
  customApiConfig?: Partial<EnvironmentConfig>;
42
- developmentMode?: boolean;
43
42
  debugMode?: boolean;
44
43
  storeId?: string;
45
44
  accountId?: string;
46
- mockData?: {
47
- customer?: Partial<Customer>;
48
- session?: Partial<Session>;
49
- store?: Partial<Store>;
50
- };
51
45
  }
52
- export declare function TagadaProvider({ children, environment, customApiConfig, developmentMode, debugMode, // Remove default, will be set based on environment
53
- storeId, accountId, mockData, }: TagadaProviderProps): import("react/jsx-runtime").JSX.Element;
46
+ export declare function TagadaProvider({ children, environment, customApiConfig, debugMode, // Remove default, will be set based on environment
47
+ storeId, accountId, }: TagadaProviderProps): import("react/jsx-runtime").JSX.Element;
54
48
  export declare function useTagadaContext(): TagadaContextValue;
55
49
  export {};
@@ -29,8 +29,8 @@ const InitializationLoader = () => (_jsx("div", { style: {
29
29
  }
30
30
  ` }) }));
31
31
  const TagadaContext = createContext(null);
32
- export function TagadaProvider({ children, environment, customApiConfig, developmentMode = false, debugMode, // Remove default, will be set based on environment
33
- storeId, accountId, mockData, }) {
32
+ export function TagadaProvider({ children, environment, customApiConfig, debugMode, // Remove default, will be set based on environment
33
+ storeId, accountId, }) {
34
34
  const [isLoading, setIsLoading] = useState(true);
35
35
  const [isInitialized, setIsInitialized] = useState(false);
36
36
  const [token, setToken] = useState(null);
@@ -284,49 +284,6 @@ storeId, accountId, mockData, }) {
284
284
  try {
285
285
  console.debug('[SDK] Initializing token...');
286
286
  setIsLoading(true);
287
- if (developmentMode) {
288
- console.debug('[SDK] Development mode: Using mock data');
289
- // Use mock data in development mode
290
- if (mockData?.customer) {
291
- setCustomer({
292
- id: 'dev-customer-123',
293
- email: 'dev@example.com',
294
- firstName: 'John',
295
- lastName: 'Doe',
296
- phone: '+1234567890',
297
- isAuthenticated: true,
298
- role: 'authenticated',
299
- ...mockData.customer,
300
- });
301
- }
302
- if (mockData?.session) {
303
- setSession({
304
- sessionId: 'dev-session-123',
305
- storeId: 'dev-store-123',
306
- accountId: 'dev-account-123',
307
- customerId: 'dev-customer-123',
308
- role: 'authenticated',
309
- isValid: true,
310
- isLoading: false,
311
- ...mockData.session,
312
- });
313
- }
314
- if (mockData?.store) {
315
- setStore({
316
- id: 'dev-store-123',
317
- name: 'Development Store',
318
- domain: 'dev.localhost',
319
- currency: 'USD',
320
- locale: 'en-US',
321
- presentmentCurrencies: ['USD'],
322
- chargeCurrencies: ['USD'],
323
- ...mockData.store,
324
- });
325
- }
326
- setIsInitialized(true);
327
- setIsLoading(false);
328
- return;
329
- }
330
287
  // Check for existing token
331
288
  const existingToken = getClientToken();
332
289
  let tokenToUse = null;
@@ -374,7 +331,7 @@ storeId, accountId, mockData, }) {
374
331
  }
375
332
  };
376
333
  void initializeToken();
377
- }, [developmentMode, mockData, storeId, createAnonymousToken, initializeSession]);
334
+ }, [storeId, createAnonymousToken, initializeSession]);
378
335
  // Update auth state when customer/session changes
379
336
  useEffect(() => {
380
337
  setAuth({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagadapay/plugin-sdk",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Modern React SDK for building Tagada Pay plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -34,8 +34,8 @@
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
36
  "@basis-theory/apple-pay-js": "^2.0.2",
37
- "@basis-theory/basis-theory-js": "^4.28.2",
38
- "@basis-theory/basis-theory-react": "^1.32.4",
37
+ "@basis-theory/basis-theory-js": "^4.30.0",
38
+ "@basis-theory/basis-theory-react": "^1.32.5",
39
39
  "@basis-theory/web-threeds": "^1.0.1",
40
40
  "axios": "^1.6.0",
41
41
  "react-intl": "^7.1.11"