@tagadapay/plugin-sdk 4.0.6 → 4.1.0

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.
Files changed (43) hide show
  1. package/README.md +25 -46
  2. package/dist/external-tracker.js +103 -36
  3. package/dist/external-tracker.min.js +2 -2
  4. package/dist/external-tracker.min.js.map +3 -3
  5. package/dist/react/types.d.ts +2 -2
  6. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  7. package/dist/tagada-react-sdk-minimal.min.js.map +3 -3
  8. package/dist/tagada-react-sdk.js +148 -28
  9. package/dist/tagada-react-sdk.min.js +2 -2
  10. package/dist/tagada-react-sdk.min.js.map +4 -4
  11. package/dist/tagada-sdk.js +125 -45
  12. package/dist/tagada-sdk.min.js +2 -2
  13. package/dist/tagada-sdk.min.js.map +4 -4
  14. package/dist/v2/core/funnelClient.js +14 -9
  15. package/dist/v2/core/pixelMapping.d.ts +84 -0
  16. package/dist/v2/core/pixelMapping.js +102 -0
  17. package/dist/v2/core/pixelTracker.d.ts +1 -6
  18. package/dist/v2/core/pixelTracker.js +36 -2
  19. package/dist/v2/core/resources/credits.d.ts +13 -0
  20. package/dist/v2/core/resources/credits.js +7 -0
  21. package/dist/v2/core/resources/offers.d.ts +5 -1
  22. package/dist/v2/core/resources/offers.js +3 -2
  23. package/dist/v2/core/resources/payments.d.ts +1 -0
  24. package/dist/v2/core/resources/payments.js +1 -0
  25. package/dist/v2/core/types.d.ts +17 -2
  26. package/dist/v2/core/utils/authHandoff.d.ts +2 -1
  27. package/dist/v2/index.d.ts +3 -1
  28. package/dist/v2/index.js +4 -1
  29. package/dist/v2/react/components/FunnelScriptInjector.js +42 -7
  30. package/dist/v2/react/hooks/useAuth.d.ts +1 -0
  31. package/dist/v2/react/hooks/useAuth.js +1 -0
  32. package/dist/v2/react/hooks/useClubOffers.d.ts +16 -0
  33. package/dist/v2/react/hooks/useClubOffers.js +29 -3
  34. package/dist/v2/react/hooks/useCustomer.d.ts +1 -0
  35. package/dist/v2/react/hooks/useCustomer.js +1 -0
  36. package/dist/v2/react/hooks/useStore.d.ts +5 -0
  37. package/dist/v2/react/hooks/useStore.js +16 -0
  38. package/dist/v2/react/index.d.ts +1 -0
  39. package/dist/v2/react/index.js +1 -0
  40. package/dist/v2/standalone/index.js +134 -46
  41. package/dist/v2/standalone/payment-service.d.ts +2 -1
  42. package/dist/v2/standalone/payment-service.js +6 -4
  43. package/package.json +113 -115
@@ -1,3 +1,4 @@
1
+ import { RedeemOfferResponse } from '../../core/resources/credits';
1
2
  export interface ClubOfferItem {
2
3
  id: string;
3
4
  productId: string;
@@ -48,6 +49,12 @@ export interface ClubOffer {
48
49
  };
49
50
  summaries: ClubOfferSummary[];
50
51
  offerLineItems: ClubOfferLineItem[];
52
+ /**
53
+ * Credits needed to redeem this offer with credits, resolved server-side.
54
+ * null when the offer is not redeemable with credits (credit system off,
55
+ * recurring line item, or no resolvable credit price).
56
+ */
57
+ creditsRequired: number | null;
51
58
  }
52
59
  export interface UseClubOffersOptions {
53
60
  /**
@@ -97,5 +104,14 @@ export interface UseClubOffersResult {
97
104
  * Pay for a club offer
98
105
  */
99
106
  payOffer: (offerId: string, returnUrl?: string) => Promise<void>;
107
+ /**
108
+ * Redeem a club offer's full bundle using credits.
109
+ * Resolves with the redemption result (order id, credits spent, remaining).
110
+ */
111
+ redeemOffer: (offerId: string) => Promise<RedeemOfferResponse>;
112
+ /**
113
+ * Whether an offer redemption is currently in flight
114
+ */
115
+ isRedeeming: boolean;
100
116
  }
101
117
  export declare function useClubOffers(options?: UseClubOffersOptions): UseClubOffersResult;
@@ -6,6 +6,7 @@
6
6
  import { useMutation, useQuery } from '@tanstack/react-query';
7
7
  import { useCallback, useMemo } from 'react';
8
8
  import { OffersResource } from '../../core/resources/offers';
9
+ import { CreditsResource } from '../../core/resources/credits';
9
10
  import { useTagadaContext } from '../providers/TagadaProvider';
10
11
  import { getGlobalApiClient } from './useApiQuery';
11
12
  import { usePluginConfig } from './usePluginConfig';
@@ -38,12 +39,14 @@ function transformToClubOffer(offer) {
38
39
  })),
39
40
  })),
40
41
  offerLineItems: [], // Not available in base Offer
42
+ // Resolved server-side and attached to the list response (not part of base Offer).
43
+ creditsRequired: offer.creditsRequired ?? null,
41
44
  };
42
45
  }
43
46
  export function useClubOffers(options = {}) {
44
47
  const { enabled = true, returnUrl } = options;
45
48
  const { storeId } = usePluginConfig();
46
- const { isSessionInitialized } = useTagadaContext();
49
+ const { isSessionInitialized, session } = useTagadaContext();
47
50
  // Create offers resource client
48
51
  const offersResource = useMemo(() => {
49
52
  try {
@@ -54,6 +57,16 @@ export function useClubOffers(options = {}) {
54
57
  (error instanceof Error ? error.message : 'Unknown error'));
55
58
  }
56
59
  }, []);
60
+ // Create credits resource client (for credit redemption of offers)
61
+ const creditsResource = useMemo(() => {
62
+ try {
63
+ return new CreditsResource(getGlobalApiClient());
64
+ }
65
+ catch (error) {
66
+ throw new Error('Failed to initialize credits resource: ' +
67
+ (error instanceof Error ? error.message : 'Unknown error'));
68
+ }
69
+ }, []);
57
70
  // Create query key
58
71
  const queryKey = useMemo(() => ['club-offers', { storeId }], [storeId]);
59
72
  // Fetch club offers using TanStack Query
@@ -80,13 +93,21 @@ export function useClubOffers(options = {}) {
80
93
  // Pay offer mutation
81
94
  const payOfferMutation = useMutation({
82
95
  mutationFn: async ({ offerId, returnUrl }) => {
83
- const url = returnUrl || (typeof window !== 'undefined' ? window.location.href : '');
84
- return await offersResource.payOffer(offerId);
96
+ return await offersResource.payOffer(offerId, undefined, undefined, session?.customerId);
85
97
  },
86
98
  onError: (error) => {
87
99
  console.error('[SDK] Failed to pay offer:', error);
88
100
  },
89
101
  });
102
+ // Redeem offer with credits mutation
103
+ const redeemOfferMutation = useMutation({
104
+ mutationFn: async ({ offerId }) => {
105
+ return await creditsResource.redeemOffer({ offerId });
106
+ },
107
+ onError: (error) => {
108
+ console.error('[SDK] Failed to redeem offer with credits:', error);
109
+ },
110
+ });
90
111
  // Helper functions
91
112
  const getOffer = useCallback((offerId) => {
92
113
  return offers.find((offer) => offer.id === offerId);
@@ -110,6 +131,9 @@ export function useClubOffers(options = {}) {
110
131
  const payOffer = useCallback(async (offerId, returnUrl) => {
111
132
  await payOfferMutation.mutateAsync({ offerId, returnUrl });
112
133
  }, [payOfferMutation]);
134
+ const redeemOffer = useCallback(async (offerId) => {
135
+ return await redeemOfferMutation.mutateAsync({ offerId });
136
+ }, [redeemOfferMutation]);
113
137
  return {
114
138
  offers,
115
139
  isLoading,
@@ -122,5 +146,7 @@ export function useClubOffers(options = {}) {
122
146
  getTotalSavings,
123
147
  clearError,
124
148
  payOffer,
149
+ redeemOffer,
150
+ isRedeeming: redeemOfferMutation.isPending,
125
151
  };
126
152
  }
@@ -7,5 +7,6 @@ export interface UseCustomerResult {
7
7
  isAuthenticated: boolean;
8
8
  isLoading: boolean;
9
9
  isAnonymous: boolean;
10
+ isVerified: boolean;
10
11
  }
11
12
  export declare function useCustomer(): UseCustomerResult;
@@ -7,5 +7,6 @@ export function useCustomer() {
7
7
  isAuthenticated: customer?.isAuthenticated || false,
8
8
  isLoading,
9
9
  isAnonymous: customer?.role === 'anonymous',
10
+ isVerified: customer?.role === 'verified',
10
11
  };
11
12
  }
@@ -0,0 +1,5 @@
1
+ export declare function useStore(): {
2
+ store: import("..").Store | null;
3
+ /** Whether the store's credit system is enabled. */
4
+ isCreditEnabled: boolean;
5
+ };
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ /**
3
+ * useStore - Hook to access the current store loaded at session init (v2)
4
+ *
5
+ * The store object (including credit settings) is delivered by the session-init
6
+ * response and held in TagadaContext, so this needs no extra network request.
7
+ */
8
+ import { useTagadaContext } from '../providers/TagadaProvider';
9
+ export function useStore() {
10
+ const { store } = useTagadaContext();
11
+ return {
12
+ store,
13
+ /** Whether the store's credit system is enabled. */
14
+ isCreditEnabled: Boolean(store?.creditSettings?.enabled),
15
+ };
16
+ }
@@ -25,6 +25,7 @@ export { useGoogleAutocomplete } from './hooks/useGoogleAutocomplete';
25
25
  export { useGooglePayCheckout } from './hooks/useGooglePayCheckout';
26
26
  export { getAvailableLanguages, useCountryOptions, useISOData, useLanguageImport, useRegionOptions } from './hooks/useISOData';
27
27
  export { useLogin } from './hooks/useLogin';
28
+ export { useStore } from './hooks/useStore';
28
29
  export { PixelTrackingProvider, usePixelTracking } from './hooks/usePixelTracking';
29
30
  export type { PixelTrackingContextValue, StandardPixelEvent } from './hooks/usePixelTracking';
30
31
  export { usePluginConfig } from './hooks/usePluginConfig';
@@ -27,6 +27,7 @@ export { useGoogleAutocomplete } from './hooks/useGoogleAutocomplete';
27
27
  export { useGooglePayCheckout } from './hooks/useGooglePayCheckout';
28
28
  export { getAvailableLanguages, useCountryOptions, useISOData, useLanguageImport, useRegionOptions } from './hooks/useISOData';
29
29
  export { useLogin } from './hooks/useLogin';
30
+ export { useStore } from './hooks/useStore';
30
31
  export { PixelTrackingProvider, usePixelTracking } from './hooks/usePixelTracking';
31
32
  export { usePluginConfig } from './hooks/usePluginConfig';
32
33
  export { useRemappableParams } from './hooks/useRemappableParams';
@@ -27,67 +27,155 @@ function parseStepConfigScripts() {
27
27
  return [];
28
28
  }
29
29
  /**
30
- * Inject a script at the specified position
30
+ * Classify a chunk found OUTSIDE <script>/<noscript> tags. Such chunks are HTML
31
+ * markup (e.g. <link rel="preconnect">, <meta>, <img>/<iframe> pixels) far more
32
+ * often than bare JS. Injecting markup as inline JS throws an uncatchable
33
+ * parse-time `SyntaxError: Unexpected token '<'` that aborts the whole <script>
34
+ * and silently kills sibling tracking scripts bundled in the same snippet.
31
35
  */
32
- function injectScript(script, index) {
33
- const position = script.position || 'body-end';
34
- const scriptId = `tagada-stepconfig-script-${index}`;
35
- // Skip if already injected
36
- if (document.getElementById(scriptId)) {
37
- return;
36
+ function classifyLooseChunk(chunk) {
37
+ if (!chunk || /^<!--[\s\S]*?-->$/.test(chunk))
38
+ return null;
39
+ const looksLikeMarkup = /^</.test(chunk) || /<\/?[a-zA-Z][\w-]*[\s/>]/.test(chunk);
40
+ return looksLikeMarkup ? { type: 'html', html: chunk } : { type: 'inline', code: chunk };
41
+ }
42
+ /**
43
+ * Parse script content that may contain multiple <script>/<noscript> tags plus
44
+ * interleaved non-script markup. Returns ordered elements to inject individually
45
+ * so one malformed chunk can never break its siblings.
46
+ */
47
+ function parseScriptContent(content) {
48
+ const trimmed = content.trim();
49
+ // No script/noscript tags → treat the whole thing as raw JS.
50
+ if (!/<(?:script|noscript)[\s>]/i.test(trimmed)) {
51
+ return trimmed ? [{ type: 'inline', code: trimmed }] : [];
38
52
  }
39
- // Extract script content (remove <script> tags if present)
40
- let scriptBody = script.content.trim();
41
- const scriptTagMatch = scriptBody.match(/^<script[^>]*>([\s\S]*)<\/script>$/i);
42
- if (scriptTagMatch) {
43
- scriptBody = scriptTagMatch[1].trim();
53
+ const elements = [];
54
+ const tagRegex = /<(script|noscript)([^>]*)>([\s\S]*?)<\/\1>/gi;
55
+ let lastIndex = 0;
56
+ let match;
57
+ while ((match = tagRegex.exec(trimmed)) !== null) {
58
+ const between = classifyLooseChunk(trimmed.slice(lastIndex, match.index).trim());
59
+ if (between)
60
+ elements.push(between);
61
+ const [, tagName, attrs, body] = match;
62
+ if (tagName.toLowerCase() === 'noscript') {
63
+ if (body.trim())
64
+ elements.push({ type: 'noscript', html: body.trim() });
65
+ }
66
+ else {
67
+ const srcMatch = attrs.match(/src=["']([^"']+)["']/i);
68
+ if (srcMatch) {
69
+ elements.push({
70
+ type: 'external',
71
+ src: srcMatch[1],
72
+ async: /\basync\b/i.test(attrs),
73
+ defer: /\bdefer\b/i.test(attrs),
74
+ });
75
+ }
76
+ if (body.trim())
77
+ elements.push({ type: 'inline', code: body.trim() });
78
+ }
79
+ lastIndex = match.index + match[0].length;
44
80
  }
45
- if (!scriptBody)
46
- return;
47
- // Wrap script content with error handling
48
- // NOTE: Use string concatenation instead of template literals to avoid breaking
49
- // when scriptBody contains backticks or ${...} expressions
50
- const wrappedScript = '(function() {\n' +
51
- ' try {\n' +
52
- ' // Script: ' + script.name + '\n' +
53
- scriptBody + '\n' +
54
- ' } catch (error) {\n' +
55
- ' console.error("[TagadaPay] StepConfig script error:", error);\n' +
56
- ' }\n' +
57
- '})();';
58
- // Create script element
59
- const scriptElement = document.createElement('script');
60
- scriptElement.id = scriptId;
61
- scriptElement.setAttribute('data-tagada-stepconfig-script', 'true');
62
- scriptElement.setAttribute('data-script-name', script.name);
63
- scriptElement.textContent = wrappedScript;
64
- // Inject at the correct position
81
+ const trailing = classifyLooseChunk(trimmed.slice(lastIndex).trim());
82
+ if (trailing)
83
+ elements.push(trailing);
84
+ return elements;
85
+ }
86
+ /** Inject a DOM element at the requested position. */
87
+ function injectAtPosition(element, position) {
65
88
  switch (position) {
66
89
  case 'head-start':
67
- if (document.head.firstChild) {
68
- document.head.insertBefore(scriptElement, document.head.firstChild);
69
- }
70
- else {
71
- document.head.appendChild(scriptElement);
72
- }
90
+ if (document.head.firstChild)
91
+ document.head.insertBefore(element, document.head.firstChild);
92
+ else
93
+ document.head.appendChild(element);
73
94
  break;
74
95
  case 'head-end':
75
- document.head.appendChild(scriptElement);
96
+ document.head.appendChild(element);
76
97
  break;
77
98
  case 'body-start':
78
- if (document.body.firstChild) {
79
- document.body.insertBefore(scriptElement, document.body.firstChild);
80
- }
81
- else {
82
- document.body.appendChild(scriptElement);
83
- }
99
+ if (document.body.firstChild)
100
+ document.body.insertBefore(element, document.body.firstChild);
101
+ else
102
+ document.body.appendChild(element);
84
103
  break;
85
104
  case 'body-end':
86
105
  default:
87
- document.body.appendChild(scriptElement);
106
+ document.body.appendChild(element);
88
107
  break;
89
108
  }
90
109
  }
110
+ /**
111
+ * Inject a (possibly multi-tag) script at the specified position.
112
+ */
113
+ function injectScript(script, index) {
114
+ const position = script.position || 'body-end';
115
+ const content = script.content.trim();
116
+ if (!content)
117
+ return;
118
+ // Skip if any element from this script is already in the DOM.
119
+ if (document.querySelector(`[data-tagada-stepconfig-index="${index}"]`)) {
120
+ return;
121
+ }
122
+ parseScriptContent(content).forEach((element, elemIndex) => {
123
+ const elemId = `tagada-stepconfig-script-${index}-${elemIndex}`;
124
+ if (element.type === 'external') {
125
+ const el = document.createElement('script');
126
+ el.id = elemId;
127
+ el.setAttribute('data-tagada-stepconfig-script', 'true');
128
+ el.setAttribute('data-tagada-stepconfig-index', String(index));
129
+ el.setAttribute('data-script-name', script.name);
130
+ el.src = element.src;
131
+ if (element.async)
132
+ el.async = true;
133
+ if (element.defer)
134
+ el.defer = true;
135
+ injectAtPosition(el, position);
136
+ }
137
+ else if (element.type === 'inline') {
138
+ // Inline JS — wrap with error handling. String concat (not template
139
+ // literals) so merchant code with backticks / ${...} stays intact.
140
+ const el = document.createElement('script');
141
+ el.id = elemId;
142
+ el.setAttribute('data-tagada-stepconfig-script', 'true');
143
+ el.setAttribute('data-tagada-stepconfig-index', String(index));
144
+ el.setAttribute('data-script-name', script.name);
145
+ el.textContent = '(function() {\n' +
146
+ ' try {\n' +
147
+ ' // Script: ' + script.name + '\n' +
148
+ element.code + '\n' +
149
+ ' } catch (error) {\n' +
150
+ ' console.error("[TagadaPay] StepConfig script error:", error);\n' +
151
+ ' }\n' +
152
+ '})();';
153
+ injectAtPosition(el, position);
154
+ }
155
+ else if (element.type === 'noscript') {
156
+ const el = document.createElement('noscript');
157
+ el.id = elemId;
158
+ el.setAttribute('data-tagada-stepconfig-script', 'true');
159
+ el.setAttribute('data-tagada-stepconfig-index', String(index));
160
+ el.innerHTML = element.html;
161
+ injectAtPosition(el, position);
162
+ }
163
+ else if (element.type === 'html') {
164
+ // Non-script markup (link/meta/img/iframe pixels) — inject as real DOM.
165
+ const template = document.createElement('template');
166
+ template.innerHTML = element.html;
167
+ Array.from(template.content.childNodes).forEach(node => {
168
+ if (node.nodeType !== 1 /* ELEMENT_NODE */)
169
+ return;
170
+ const el = node;
171
+ el.setAttribute('data-tagada-stepconfig-script', 'true');
172
+ el.setAttribute('data-tagada-stepconfig-index', String(index));
173
+ el.setAttribute('data-script-name', script.name);
174
+ injectAtPosition(el, position);
175
+ });
176
+ }
177
+ });
178
+ }
91
179
  /**
92
180
  * Auto-inject all stepConfig scripts
93
181
  * Called automatically when SDK loads
@@ -16,8 +16,8 @@
16
16
  *
17
17
  * No React. No hooks. No DOM framework dependency.
18
18
  */
19
- import { PaymentsResource, type Payment, type CardPaymentMethod, type ApplePayToken, type GooglePayToken, type PaymentInstrumentResponse, type PaymentInstrumentCustomerResponse } from '../core/resources/payments';
20
19
  import type { ApiClient } from '../core/resources/apiClient';
20
+ import { PaymentsResource, type ApplePayToken, type CardPaymentMethod, type GooglePayToken, type Payment, type PaymentInstrumentCustomerResponse, type PaymentInstrumentResponse } from '../core/resources/payments';
21
21
  export interface PaymentServiceConfig {
22
22
  apiClient: ApiClient;
23
23
  storeId?: string;
@@ -30,6 +30,7 @@ export interface CardData {
30
30
  export interface ApmData {
31
31
  processorId: string;
32
32
  paymentMethod: string;
33
+ blikCode?: string;
33
34
  initiatedBy?: 'customer' | 'merchant';
34
35
  source?: 'upsell' | 'checkout' | 'offer' | 'missing_club' | 'forced';
35
36
  }
@@ -16,11 +16,11 @@
16
16
  *
17
17
  * No React. No hooks. No DOM framework dependency.
18
18
  */
19
- import { PaymentsResource, } from '../core/resources/payments';
20
- import { ThreedsResource } from '../core/resources/threeds';
21
- import { StoreConfigResource } from '../core/resources/storeConfig';
22
- import { getAssignedPaymentFlowId } from '../core/funnelClient';
23
19
  import { getBasisTheoryApiKey } from '../../react/config/payment';
20
+ import { getAssignedPaymentFlowId } from '../core/funnelClient';
21
+ import { PaymentsResource } from '../core/resources/payments';
22
+ import { StoreConfigResource } from '../core/resources/storeConfig';
23
+ import { ThreedsResource } from '../core/resources/threeds';
24
24
  // =============================================================================
25
25
  // PAYMENT SERVICE
26
26
  // =============================================================================
@@ -212,6 +212,7 @@ export class PaymentService {
212
212
  paymentFlowId,
213
213
  processorId: extra?.processorId,
214
214
  paymentMethod: extra?.paymentMethod,
215
+ blikCode: extra?.blikCode,
215
216
  shippingRateId: extra?.shippingRateId,
216
217
  });
217
218
  console.log('[PaymentService] Payment response:', {
@@ -1052,6 +1053,7 @@ export class PaymentService {
1052
1053
  return await this.processAndHandle(checkoutSessionId, '', undefined, {
1053
1054
  processorId: apmData.processorId,
1054
1055
  paymentMethod: apmData.paymentMethod,
1056
+ blikCode: apmData.blikCode,
1055
1057
  initiatedBy: apmData.initiatedBy,
1056
1058
  source: apmData.source,
1057
1059
  shippingRateId: options?.shippingRateId,