@tagadapay/plugin-sdk 2.8.9 → 3.0.1
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.
- package/README.md +14 -14
- package/dist/index.js +1 -1
- package/dist/react/hooks/usePluginConfig.d.ts +1 -0
- package/dist/react/hooks/usePluginConfig.js +69 -18
- package/dist/react/providers/TagadaProvider.js +1 -4
- package/dist/v2/core/client.d.ts +22 -0
- package/dist/v2/core/client.js +90 -1
- package/dist/v2/core/config/environment.d.ts +24 -2
- package/dist/v2/core/config/environment.js +58 -25
- package/dist/v2/core/funnelClient.d.ts +84 -0
- package/dist/v2/core/funnelClient.js +252 -0
- package/dist/v2/core/index.d.ts +2 -0
- package/dist/v2/core/index.js +3 -0
- package/dist/v2/core/resources/apiClient.d.ts +5 -0
- package/dist/v2/core/resources/apiClient.js +47 -0
- package/dist/v2/core/resources/funnel.d.ts +8 -0
- package/dist/v2/core/resources/offers.d.ts +182 -8
- package/dist/v2/core/resources/offers.js +25 -0
- package/dist/v2/core/resources/products.d.ts +5 -0
- package/dist/v2/core/resources/products.js +15 -1
- package/dist/v2/core/types.d.ts +1 -0
- package/dist/v2/core/utils/funnelQueryKeys.d.ts +23 -0
- package/dist/v2/core/utils/funnelQueryKeys.js +23 -0
- package/dist/v2/core/utils/index.d.ts +2 -0
- package/dist/v2/core/utils/index.js +2 -0
- package/dist/v2/core/utils/pluginConfig.d.ts +1 -0
- package/dist/v2/core/utils/pluginConfig.js +84 -22
- package/dist/v2/core/utils/sessionStorage.d.ts +20 -0
- package/dist/v2/core/utils/sessionStorage.js +39 -0
- package/dist/v2/index.d.ts +3 -2
- package/dist/v2/index.js +1 -1
- package/dist/v2/react/components/ApplePayButton.js +1 -1
- package/dist/v2/react/hooks/__examples__/FunnelContextExample.d.ts +3 -0
- package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +4 -3
- package/dist/v2/react/hooks/useClubOffers.d.ts +2 -2
- package/dist/v2/react/hooks/useFunnel.d.ts +27 -38
- package/dist/v2/react/hooks/useFunnel.js +22 -660
- package/dist/v2/react/hooks/useFunnelLegacy.d.ts +52 -0
- package/dist/v2/react/hooks/useFunnelLegacy.js +733 -0
- package/dist/v2/react/hooks/useOfferQuery.d.ts +109 -0
- package/dist/v2/react/hooks/useOfferQuery.js +483 -0
- package/dist/v2/react/hooks/useOffersQuery.d.ts +10 -58
- package/dist/v2/react/hooks/useOffersQuery.js +110 -8
- package/dist/v2/react/hooks/useProductsQuery.d.ts +1 -0
- package/dist/v2/react/hooks/useProductsQuery.js +10 -6
- package/dist/v2/react/index.d.ts +7 -4
- package/dist/v2/react/index.js +4 -2
- package/dist/v2/react/providers/TagadaProvider.d.ts +45 -2
- package/dist/v2/react/providers/TagadaProvider.js +116 -3
- package/dist/v2/standalone/index.d.ts +20 -0
- package/dist/v2/standalone/index.js +22 -0
- package/dist/v2/vue/index.d.ts +6 -0
- package/dist/v2/vue/index.js +10 -0
- package/package.json +6 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ApiClient } from './resources/apiClient';
|
|
2
|
+
import { SimpleFunnelContext, FunnelAction, FunnelNavigationResult } from './resources/funnel';
|
|
3
|
+
import { PluginConfig } from './utils/pluginConfig';
|
|
4
|
+
export interface FunnelClientConfig {
|
|
5
|
+
apiClient: ApiClient;
|
|
6
|
+
debugMode?: boolean;
|
|
7
|
+
pluginConfig?: PluginConfig;
|
|
8
|
+
environment?: {
|
|
9
|
+
environment: 'local' | 'development' | 'production' | 'staging';
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Automatically redirect to result.url after navigation (default: true)
|
|
13
|
+
* Set to false if you want to handle navigation manually
|
|
14
|
+
*/
|
|
15
|
+
autoRedirect?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface FunnelState {
|
|
18
|
+
context: SimpleFunnelContext | null;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
isInitialized: boolean;
|
|
21
|
+
isNavigating: boolean;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
sessionError: Error | null;
|
|
24
|
+
}
|
|
25
|
+
export declare class FunnelClient {
|
|
26
|
+
state: FunnelState;
|
|
27
|
+
private resource;
|
|
28
|
+
private eventDispatcher;
|
|
29
|
+
private config;
|
|
30
|
+
private isInitializing;
|
|
31
|
+
private initializationAttempted;
|
|
32
|
+
constructor(config: FunnelClientConfig);
|
|
33
|
+
/**
|
|
34
|
+
* Update configuration (e.g. when plugin config loads)
|
|
35
|
+
*/
|
|
36
|
+
setConfig(config: Partial<FunnelClientConfig>): void;
|
|
37
|
+
/**
|
|
38
|
+
* Subscribe to state changes
|
|
39
|
+
*/
|
|
40
|
+
subscribe(listener: (state: FunnelState) => void): () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Get current state
|
|
43
|
+
*/
|
|
44
|
+
getState(): FunnelState;
|
|
45
|
+
/**
|
|
46
|
+
* Initialize session with automatic detection (cookies, URL, etc.)
|
|
47
|
+
*/
|
|
48
|
+
autoInitialize(authSession: {
|
|
49
|
+
customerId: string;
|
|
50
|
+
sessionId: string;
|
|
51
|
+
}, store: {
|
|
52
|
+
id: string;
|
|
53
|
+
accountId: string;
|
|
54
|
+
}, funnelId?: string): Promise<SimpleFunnelContext | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Manual initialization
|
|
57
|
+
*/
|
|
58
|
+
initialize(authSession: {
|
|
59
|
+
customerId: string;
|
|
60
|
+
sessionId: string;
|
|
61
|
+
}, store: {
|
|
62
|
+
id: string;
|
|
63
|
+
accountId: string;
|
|
64
|
+
}, funnelId?: string, entryStepId?: string): Promise<SimpleFunnelContext<{}>>;
|
|
65
|
+
/**
|
|
66
|
+
* Navigate
|
|
67
|
+
*/
|
|
68
|
+
navigate(event: FunnelAction): Promise<FunnelNavigationResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Refresh session data
|
|
71
|
+
*/
|
|
72
|
+
refreshSession(): Promise<SimpleFunnelContext<{}> | undefined>;
|
|
73
|
+
/**
|
|
74
|
+
* Update context data
|
|
75
|
+
*/
|
|
76
|
+
updateContext(updates: Partial<SimpleFunnelContext>): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* End session
|
|
79
|
+
*/
|
|
80
|
+
endSession(): Promise<void>;
|
|
81
|
+
private updateState;
|
|
82
|
+
private handleSessionSuccess;
|
|
83
|
+
private enrichContext;
|
|
84
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { FunnelResource } from './resources/funnel';
|
|
2
|
+
import { EventDispatcher } from './utils/eventDispatcher';
|
|
3
|
+
import { getFunnelSessionCookie, setFunnelSessionCookie } from './utils/sessionStorage';
|
|
4
|
+
import { detectEnvironment } from './config/environment';
|
|
5
|
+
export class FunnelClient {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.eventDispatcher = new EventDispatcher();
|
|
8
|
+
// Guards
|
|
9
|
+
this.isInitializing = false;
|
|
10
|
+
this.initializationAttempted = false;
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.resource = new FunnelResource(config.apiClient);
|
|
13
|
+
this.state = {
|
|
14
|
+
context: null,
|
|
15
|
+
isLoading: false,
|
|
16
|
+
isInitialized: false,
|
|
17
|
+
isNavigating: false,
|
|
18
|
+
error: null,
|
|
19
|
+
sessionError: null,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Update configuration (e.g. when plugin config loads)
|
|
24
|
+
*/
|
|
25
|
+
setConfig(config) {
|
|
26
|
+
this.config = { ...this.config, ...config };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Subscribe to state changes
|
|
30
|
+
*/
|
|
31
|
+
subscribe(listener) {
|
|
32
|
+
return this.eventDispatcher.subscribe(listener);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get current state
|
|
36
|
+
*/
|
|
37
|
+
getState() {
|
|
38
|
+
return this.state;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Initialize session with automatic detection (cookies, URL, etc.)
|
|
42
|
+
*/
|
|
43
|
+
async autoInitialize(authSession, store, funnelId) {
|
|
44
|
+
if (this.state.context)
|
|
45
|
+
return this.state.context;
|
|
46
|
+
if (this.isInitializing)
|
|
47
|
+
return null;
|
|
48
|
+
if (this.initializationAttempted)
|
|
49
|
+
return null;
|
|
50
|
+
this.initializationAttempted = true;
|
|
51
|
+
this.isInitializing = true;
|
|
52
|
+
this.updateState({ isLoading: true, error: null });
|
|
53
|
+
try {
|
|
54
|
+
// URL params
|
|
55
|
+
const params = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
|
|
56
|
+
const urlFunnelId = params.get('funnelId');
|
|
57
|
+
const effectiveFunnelId = urlFunnelId || funnelId;
|
|
58
|
+
let existingSessionId = params.get('funnelSessionId');
|
|
59
|
+
// Cookie fallback
|
|
60
|
+
if (!existingSessionId) {
|
|
61
|
+
existingSessionId = getFunnelSessionCookie() || null;
|
|
62
|
+
}
|
|
63
|
+
if (this.config.debugMode) {
|
|
64
|
+
console.log('🚀 [FunnelClient] Auto-initializing...', { existingSessionId, effectiveFunnelId });
|
|
65
|
+
}
|
|
66
|
+
// Note: We proceed even without funnelId/sessionId - the backend will create a new anonymous session if needed
|
|
67
|
+
const response = await this.resource.initialize({
|
|
68
|
+
cmsSession: {
|
|
69
|
+
customerId: authSession.customerId,
|
|
70
|
+
sessionId: authSession.sessionId,
|
|
71
|
+
storeId: store.id,
|
|
72
|
+
accountId: store.accountId,
|
|
73
|
+
},
|
|
74
|
+
funnelId: effectiveFunnelId,
|
|
75
|
+
existingSessionId: existingSessionId || undefined,
|
|
76
|
+
currentUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
77
|
+
});
|
|
78
|
+
if (response.success && response.context) {
|
|
79
|
+
const enriched = this.enrichContext(response.context);
|
|
80
|
+
this.handleSessionSuccess(enriched);
|
|
81
|
+
return enriched;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
throw new Error(response.error || 'Failed to initialize funnel session');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
89
|
+
this.updateState({ error: err, isLoading: false });
|
|
90
|
+
if (this.config.debugMode) {
|
|
91
|
+
console.error('❌ [FunnelClient] Init failed:', err);
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
this.isInitializing = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Manual initialization
|
|
101
|
+
*/
|
|
102
|
+
async initialize(authSession, store, funnelId, entryStepId) {
|
|
103
|
+
this.updateState({ isLoading: true, error: null });
|
|
104
|
+
try {
|
|
105
|
+
const response = await this.resource.initialize({
|
|
106
|
+
cmsSession: {
|
|
107
|
+
customerId: authSession.customerId,
|
|
108
|
+
sessionId: authSession.sessionId,
|
|
109
|
+
storeId: store.id,
|
|
110
|
+
accountId: store.accountId,
|
|
111
|
+
},
|
|
112
|
+
funnelId: funnelId,
|
|
113
|
+
entryStepId,
|
|
114
|
+
currentUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
115
|
+
});
|
|
116
|
+
if (response.success && response.context) {
|
|
117
|
+
const enriched = this.enrichContext(response.context);
|
|
118
|
+
this.handleSessionSuccess(enriched);
|
|
119
|
+
return enriched;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
throw new Error(response.error || 'Failed to initialize');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
127
|
+
this.updateState({ error: err, isLoading: false });
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Navigate
|
|
133
|
+
*/
|
|
134
|
+
async navigate(event) {
|
|
135
|
+
if (!this.state.context?.sessionId)
|
|
136
|
+
throw new Error('No active session');
|
|
137
|
+
this.updateState({ isNavigating: true, isLoading: true });
|
|
138
|
+
try {
|
|
139
|
+
const response = await this.resource.navigate({
|
|
140
|
+
sessionId: this.state.context.sessionId,
|
|
141
|
+
event
|
|
142
|
+
});
|
|
143
|
+
if (response.success && response.result) {
|
|
144
|
+
// Refresh session to get updated context
|
|
145
|
+
await this.refreshSession();
|
|
146
|
+
this.updateState({ isNavigating: false, isLoading: false });
|
|
147
|
+
const result = response.result;
|
|
148
|
+
// Auto-redirect if enabled (default: true) and result has a URL
|
|
149
|
+
const shouldAutoRedirect = this.config.autoRedirect !== false; // Default to true
|
|
150
|
+
if (shouldAutoRedirect && result?.url && typeof window !== 'undefined') {
|
|
151
|
+
if (this.config.debugMode) {
|
|
152
|
+
console.log('🚀 [FunnelClient] Auto-redirecting to:', result.url);
|
|
153
|
+
}
|
|
154
|
+
window.location.href = result.url;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
throw new Error(response.error || 'Navigation failed');
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
162
|
+
this.updateState({ error: err, isNavigating: false, isLoading: false });
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Refresh session data
|
|
168
|
+
*/
|
|
169
|
+
async refreshSession() {
|
|
170
|
+
if (!this.state.context?.sessionId)
|
|
171
|
+
return;
|
|
172
|
+
try {
|
|
173
|
+
const response = await this.resource.getSession(this.state.context.sessionId);
|
|
174
|
+
if (response.success && response.context) {
|
|
175
|
+
const enriched = this.enrichContext(response.context);
|
|
176
|
+
this.updateState({ context: enriched, sessionError: null });
|
|
177
|
+
return enriched;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
this.updateState({ sessionError: error instanceof Error ? error : new Error(String(error)) });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Update context data
|
|
186
|
+
*/
|
|
187
|
+
async updateContext(updates) {
|
|
188
|
+
if (!this.state.context?.sessionId)
|
|
189
|
+
throw new Error('No active session');
|
|
190
|
+
this.updateState({ isLoading: true });
|
|
191
|
+
try {
|
|
192
|
+
const response = await this.resource.updateContext(this.state.context.sessionId, { contextUpdates: updates });
|
|
193
|
+
if (response.success) {
|
|
194
|
+
await this.refreshSession();
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
throw new Error(response.error || 'Failed to update context');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
this.updateState({ isLoading: false });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* End session
|
|
206
|
+
*/
|
|
207
|
+
async endSession() {
|
|
208
|
+
if (!this.state.context?.sessionId)
|
|
209
|
+
return;
|
|
210
|
+
try {
|
|
211
|
+
await this.resource.endSession(this.state.context.sessionId);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
this.state.context = null;
|
|
215
|
+
this.updateState({ context: null, isInitialized: false });
|
|
216
|
+
if (typeof document !== 'undefined') {
|
|
217
|
+
// Clear cookie via import or manually if needed, but we have utility for that
|
|
218
|
+
// We should probably import clearFunnelSessionCookie
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Private helpers
|
|
223
|
+
updateState(updates) {
|
|
224
|
+
this.state = { ...this.state, ...updates };
|
|
225
|
+
this.eventDispatcher.notify(this.state);
|
|
226
|
+
}
|
|
227
|
+
handleSessionSuccess(context) {
|
|
228
|
+
setFunnelSessionCookie(context.sessionId);
|
|
229
|
+
this.updateState({
|
|
230
|
+
context,
|
|
231
|
+
isLoading: false,
|
|
232
|
+
isInitialized: true,
|
|
233
|
+
error: null,
|
|
234
|
+
sessionError: null
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
enrichContext(ctx) {
|
|
238
|
+
const env = this.config.environment?.environment || detectEnvironment();
|
|
239
|
+
if (env !== 'local')
|
|
240
|
+
return ctx;
|
|
241
|
+
const localResources = this.config.pluginConfig?.staticResources || {};
|
|
242
|
+
if (Object.keys(localResources).length === 0)
|
|
243
|
+
return ctx;
|
|
244
|
+
return {
|
|
245
|
+
...ctx,
|
|
246
|
+
static: {
|
|
247
|
+
...localResources,
|
|
248
|
+
...(ctx.static || {})
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
package/dist/v2/core/index.d.ts
CHANGED
package/dist/v2/core/index.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
export * from './utils';
|
|
7
7
|
// Export resources (axios-based API clients)
|
|
8
8
|
export * from './resources';
|
|
9
|
+
// Export clients (stateful logic)
|
|
10
|
+
export * from './client';
|
|
11
|
+
export * from './funnelClient';
|
|
9
12
|
// Export path remapping helpers (framework-agnostic)
|
|
10
13
|
export * from './pathRemapping';
|
|
11
14
|
// Export legacy files that are still needed
|
|
@@ -18,6 +18,9 @@ export declare class ApiClient {
|
|
|
18
18
|
axios: AxiosInstance;
|
|
19
19
|
private currentToken;
|
|
20
20
|
private tokenProvider;
|
|
21
|
+
private requestHistory;
|
|
22
|
+
private readonly WINDOW_MS;
|
|
23
|
+
private readonly MAX_REQUESTS;
|
|
21
24
|
constructor(config: ApiClientConfig);
|
|
22
25
|
setTokenProvider(provider: TokenProvider): void;
|
|
23
26
|
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>;
|
|
@@ -30,4 +33,6 @@ export declare class ApiClient {
|
|
|
30
33
|
updateToken(token: string | null): void;
|
|
31
34
|
getCurrentToken(): string | null;
|
|
32
35
|
updateConfig(config: Partial<ApiClientConfig>): void;
|
|
36
|
+
private checkRequestLimit;
|
|
37
|
+
private cleanupHistory;
|
|
33
38
|
}
|
|
@@ -7,6 +7,10 @@ export class ApiClient {
|
|
|
7
7
|
constructor(config) {
|
|
8
8
|
this.currentToken = null;
|
|
9
9
|
this.tokenProvider = null;
|
|
10
|
+
// Circuit breaker state
|
|
11
|
+
this.requestHistory = new Map();
|
|
12
|
+
this.WINDOW_MS = 5000; // 5 seconds window
|
|
13
|
+
this.MAX_REQUESTS = 30; // Max 30 requests per endpoint in window
|
|
10
14
|
this.axios = axios.create({
|
|
11
15
|
baseURL: config.baseURL,
|
|
12
16
|
timeout: config.timeout || 30000,
|
|
@@ -15,8 +19,22 @@ export class ApiClient {
|
|
|
15
19
|
...config.headers,
|
|
16
20
|
},
|
|
17
21
|
});
|
|
22
|
+
// Cleanup interval for circuit breaker history
|
|
23
|
+
if (typeof setInterval !== 'undefined') {
|
|
24
|
+
setInterval(() => this.cleanupHistory(), 10000);
|
|
25
|
+
}
|
|
18
26
|
// Request interceptor for logging and auth
|
|
19
27
|
this.axios.interceptors.request.use(async (config) => {
|
|
28
|
+
// Circuit Breaker Check
|
|
29
|
+
if (config.url) {
|
|
30
|
+
try {
|
|
31
|
+
this.checkRequestLimit(`${config.method?.toUpperCase()}:${config.url}`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error('[SDK] 🛑 Request blocked by Circuit Breaker:', error);
|
|
35
|
+
return Promise.reject(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
20
38
|
// Check if we need to wait for token
|
|
21
39
|
if (!config.skipAuth && !this.currentToken && this.tokenProvider) {
|
|
22
40
|
try {
|
|
@@ -113,4 +131,33 @@ export class ApiClient {
|
|
|
113
131
|
}
|
|
114
132
|
console.log('[SDK] ApiClient configuration updated');
|
|
115
133
|
}
|
|
134
|
+
// Circuit Breaker Implementation
|
|
135
|
+
checkRequestLimit(key) {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
const history = this.requestHistory.get(key);
|
|
138
|
+
if (!history) {
|
|
139
|
+
this.requestHistory.set(key, { count: 1, firstRequestTime: now });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (now - history.firstRequestTime > this.WINDOW_MS) {
|
|
143
|
+
// Window expired, reset
|
|
144
|
+
this.requestHistory.set(key, { count: 1, firstRequestTime: now });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
history.count++;
|
|
148
|
+
if (history.count > this.MAX_REQUESTS) {
|
|
149
|
+
const error = new Error(`Circuit Breaker: Too many requests to ${key} (${history.count} in ${this.WINDOW_MS}ms)`);
|
|
150
|
+
// Add a property to identify this as a circuit breaker error
|
|
151
|
+
error.isCircuitBreaker = true;
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
cleanupHistory() {
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
for (const [key, history] of this.requestHistory.entries()) {
|
|
158
|
+
if (now - history.firstRequestTime > this.WINDOW_MS) {
|
|
159
|
+
this.requestHistory.delete(key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
116
163
|
}
|
|
@@ -344,6 +344,7 @@ export interface FunnelNavigationAction {
|
|
|
344
344
|
}
|
|
345
345
|
export interface FunnelNavigationResult {
|
|
346
346
|
stepId: string;
|
|
347
|
+
url?: string;
|
|
347
348
|
action: FunnelNavigationAction;
|
|
348
349
|
context: SimpleFunnelContext;
|
|
349
350
|
tracking?: {
|
|
@@ -408,6 +409,13 @@ export interface SimpleFunnelContext<TCustom = {}> {
|
|
|
408
409
|
* - Standard keys provide IntelliSense, custom keys always allowed
|
|
409
410
|
*/
|
|
410
411
|
resources?: FunnelResourceMap<TCustom>;
|
|
412
|
+
/**
|
|
413
|
+
* Static resources from plugin manifest (type: "static")
|
|
414
|
+
* - Configured in funnel editor's Static Resources tab
|
|
415
|
+
* - Available at runtime as context.static
|
|
416
|
+
* - Example: context.static.offer.id for statically configured offers
|
|
417
|
+
*/
|
|
418
|
+
static?: Record<string, any>;
|
|
411
419
|
/**
|
|
412
420
|
* Legacy/Custom metadata
|
|
413
421
|
* For backward compatibility and flexible unstructured data
|
|
@@ -3,44 +3,205 @@
|
|
|
3
3
|
* Axios-based API client for offers endpoints
|
|
4
4
|
*/
|
|
5
5
|
import { ApiClient } from './apiClient';
|
|
6
|
-
|
|
6
|
+
import type { CheckoutSessionState, OrderSummary, OrderSummaryItem, VariantOption } from './postPurchases';
|
|
7
|
+
export type { CheckoutSessionState, OrderSummary, OrderSummaryItem, VariantOption };
|
|
8
|
+
/**
|
|
9
|
+
* Currency option with rate, amount, and lock info
|
|
10
|
+
*/
|
|
11
|
+
export interface OfferCurrencyOption {
|
|
12
|
+
rate: number;
|
|
13
|
+
amount: number;
|
|
14
|
+
lock?: boolean;
|
|
15
|
+
date?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Product info within an offer
|
|
19
|
+
*/
|
|
20
|
+
export interface OfferProduct {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description: string | null;
|
|
24
|
+
storeId?: string;
|
|
25
|
+
externalProductId?: string | null;
|
|
26
|
+
externalCollectionIds?: string[] | null;
|
|
27
|
+
active?: boolean;
|
|
28
|
+
isShippable?: boolean;
|
|
29
|
+
isTaxable?: boolean;
|
|
30
|
+
taxCategory?: string;
|
|
31
|
+
unitLabel?: string;
|
|
32
|
+
accountingCode?: string;
|
|
33
|
+
accountId?: string;
|
|
34
|
+
autoSync?: boolean;
|
|
35
|
+
creditPrice?: number | null;
|
|
36
|
+
createdAt?: string;
|
|
37
|
+
updatedAt?: string;
|
|
38
|
+
storefrontRawData?: any;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Variant info within an offer
|
|
42
|
+
*/
|
|
43
|
+
export interface OfferVariant {
|
|
44
|
+
id: string;
|
|
45
|
+
productId?: string;
|
|
46
|
+
default?: boolean;
|
|
47
|
+
name: string;
|
|
48
|
+
description?: string | null;
|
|
49
|
+
sku?: string;
|
|
50
|
+
externalVariantId?: string | null;
|
|
51
|
+
price?: number | null;
|
|
52
|
+
currency?: string | null;
|
|
53
|
+
compareAtPrice?: number | null;
|
|
54
|
+
grams?: number | null;
|
|
55
|
+
imageUrl: string | null;
|
|
56
|
+
active?: boolean;
|
|
57
|
+
accountId?: string | null;
|
|
58
|
+
creditPrice?: number | null;
|
|
59
|
+
createdAt?: string;
|
|
60
|
+
updatedAt?: string;
|
|
61
|
+
product?: OfferProduct | null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Price info within an offer line item
|
|
65
|
+
*/
|
|
66
|
+
export interface OfferLineItemPrice {
|
|
7
67
|
id: string;
|
|
68
|
+
createdAt?: string;
|
|
69
|
+
updatedAt?: string;
|
|
70
|
+
accountId?: string;
|
|
71
|
+
productId?: string;
|
|
72
|
+
variantId: string;
|
|
73
|
+
default?: boolean;
|
|
74
|
+
currencyOptions: Record<string, OfferCurrencyOption>;
|
|
75
|
+
recurring?: boolean;
|
|
76
|
+
billingTiming?: string;
|
|
77
|
+
interval?: string;
|
|
78
|
+
intervalCount?: number;
|
|
79
|
+
rebillWithOrder?: boolean;
|
|
80
|
+
rebillMode?: string | null;
|
|
81
|
+
rebillStepIntervalMs?: number | null;
|
|
82
|
+
isExternalSellingPlan?: boolean;
|
|
83
|
+
isInternalSellingPlan?: boolean;
|
|
84
|
+
shopifySellingPlanId?: string | null;
|
|
85
|
+
creditsPerRebill?: number | null;
|
|
86
|
+
deliverySettings?: Record<string, any>;
|
|
87
|
+
priceSettings?: Record<string, any>;
|
|
88
|
+
variant?: OfferVariant | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Line item within an offer
|
|
92
|
+
*/
|
|
93
|
+
export interface OfferLineItem {
|
|
94
|
+
id: string;
|
|
95
|
+
offerId?: string;
|
|
96
|
+
quantity: number;
|
|
97
|
+
priceId?: string;
|
|
98
|
+
variantId?: string;
|
|
99
|
+
productId?: string;
|
|
100
|
+
price: OfferLineItemPrice | null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Summary item within an offer summary
|
|
104
|
+
*/
|
|
105
|
+
export interface OfferSummaryItem {
|
|
106
|
+
id: string;
|
|
107
|
+
productId: string;
|
|
108
|
+
variantId: string;
|
|
109
|
+
priceId?: string;
|
|
8
110
|
product: {
|
|
9
111
|
name: string;
|
|
10
112
|
description: string;
|
|
11
113
|
};
|
|
12
114
|
variant: {
|
|
13
115
|
name: string;
|
|
14
|
-
|
|
116
|
+
description?: string;
|
|
117
|
+
imageUrl: string | null;
|
|
118
|
+
grams?: number | null;
|
|
15
119
|
};
|
|
16
|
-
|
|
120
|
+
sku?: string;
|
|
17
121
|
unitAmount: number;
|
|
122
|
+
quantity: number;
|
|
18
123
|
amount: number;
|
|
19
124
|
adjustedAmount: number;
|
|
20
125
|
currency: string;
|
|
126
|
+
adjustments?: any[];
|
|
127
|
+
recurring?: boolean;
|
|
128
|
+
rebillMode?: string | null;
|
|
129
|
+
interval?: string;
|
|
130
|
+
intervalCount?: number;
|
|
131
|
+
totalBillingCycles?: number;
|
|
132
|
+
unitAmountAfterFirstCycle?: number;
|
|
133
|
+
isExternalSellingPlan?: boolean;
|
|
134
|
+
isInternalSellingPlan?: boolean;
|
|
135
|
+
deliverySettings?: Record<string, any>;
|
|
136
|
+
priceSettings?: Record<string, any>;
|
|
137
|
+
orderLineItemProduct?: {
|
|
138
|
+
name: string;
|
|
139
|
+
};
|
|
140
|
+
orderLineItemVariant?: {
|
|
141
|
+
name: string;
|
|
142
|
+
imageUrl: string | null;
|
|
143
|
+
};
|
|
144
|
+
metadata?: Record<string, any>;
|
|
21
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Offer summary containing items and totals
|
|
148
|
+
*/
|
|
22
149
|
export interface OfferSummary {
|
|
23
|
-
items:
|
|
150
|
+
items: OfferSummaryItem[];
|
|
151
|
+
totalWeight?: number;
|
|
152
|
+
subtotalAmount?: number;
|
|
153
|
+
lineItemsPromotionAmount?: number;
|
|
154
|
+
subtotalAdjustedAmount?: number;
|
|
155
|
+
totalPromotionAmount: number;
|
|
156
|
+
lineItemsTaxAmount?: number;
|
|
157
|
+
totalTaxAmount?: number;
|
|
158
|
+
shippingCost?: number;
|
|
159
|
+
shippingCostIsFree?: boolean;
|
|
24
160
|
totalAmount: number;
|
|
25
161
|
totalAdjustedAmount: number;
|
|
26
|
-
totalPromotionAmount: number;
|
|
27
|
-
currency: string;
|
|
28
162
|
adjustments: {
|
|
29
163
|
type: string;
|
|
30
164
|
description: string;
|
|
31
165
|
amount: number;
|
|
32
166
|
}[];
|
|
167
|
+
currency: string;
|
|
168
|
+
_consolidated?: boolean;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Offer promotion association
|
|
172
|
+
*/
|
|
173
|
+
export interface OfferPromotion {
|
|
174
|
+
id: string;
|
|
175
|
+
promotionId: string;
|
|
176
|
+
promotion?: {
|
|
177
|
+
id: string;
|
|
178
|
+
name: string;
|
|
179
|
+
code?: string | null;
|
|
180
|
+
automatic?: boolean;
|
|
181
|
+
};
|
|
33
182
|
}
|
|
34
|
-
|
|
35
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Main Offer type
|
|
185
|
+
*/
|
|
36
186
|
export interface Offer {
|
|
37
187
|
id: string;
|
|
188
|
+
createdAt?: string;
|
|
189
|
+
updatedAt?: string;
|
|
190
|
+
storeId?: string;
|
|
191
|
+
type?: string;
|
|
38
192
|
titleTrans: Record<string, string>;
|
|
193
|
+
expiryDate?: string | null;
|
|
39
194
|
summaries: OfferSummary[];
|
|
195
|
+
offerLineItems?: OfferLineItem[];
|
|
196
|
+
promotions?: OfferPromotion[];
|
|
40
197
|
}
|
|
41
198
|
export declare class OffersResource {
|
|
42
199
|
private apiClient;
|
|
43
200
|
constructor(apiClient: ApiClient);
|
|
201
|
+
/**
|
|
202
|
+
* Get a single offer by ID
|
|
203
|
+
*/
|
|
204
|
+
getOfferById(offerId: string): Promise<Offer>;
|
|
44
205
|
/**
|
|
45
206
|
* Get offers for a store
|
|
46
207
|
*/
|
|
@@ -70,6 +231,19 @@ export declare class OffersResource {
|
|
|
70
231
|
*/
|
|
71
232
|
payOffer(offerId: string, orderId?: string): Promise<any>;
|
|
72
233
|
/**
|
|
234
|
+
* Transform offer to checkout session with dynamic variant selection
|
|
235
|
+
* Uses lineItems from the offer to create a new checkout session
|
|
236
|
+
*/
|
|
237
|
+
transformToCheckoutSession(offerId: string, lineItems: {
|
|
238
|
+
variantId: string;
|
|
239
|
+
quantity: number;
|
|
240
|
+
}[], mainOrderId: string, returnUrl?: string): Promise<{
|
|
241
|
+
checkoutUrl: string;
|
|
242
|
+
checkoutSessionId?: string;
|
|
243
|
+
customerId?: string;
|
|
244
|
+
}>;
|
|
245
|
+
/**
|
|
246
|
+
* @deprecated Use transformToCheckoutSession instead
|
|
73
247
|
* Transform offer to checkout session with dynamic variant selection
|
|
74
248
|
*/
|
|
75
249
|
transformToCheckout(offerId: string, returnUrl?: string): Promise<{
|