@mintmoney/react 0.1.0-alpha.30 → 0.1.0-alpha.32
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/dist/esm/api/checkouts.js +3 -10
- package/dist/esm/api/checkouts.js.map +1 -1
- package/dist/esm/api/generated.js +16 -500
- package/dist/esm/api/generated.js.map +1 -1
- package/dist/esm/api/payments.js +28 -29
- package/dist/esm/api/payments.js.map +1 -1
- package/dist/esm/api/types.js.map +1 -1
- package/dist/esm/checkout/index.js +19 -1
- package/dist/esm/checkout/index.js.map +1 -1
- package/dist/esm/checkout/modal.js +7 -7
- package/dist/esm/checkout/modal.js.map +1 -1
- package/dist/esm/checkout/views/crypto-intent/direct-details.js +148 -19
- package/dist/esm/checkout/views/crypto-intent/direct-details.js.map +1 -1
- package/dist/esm/checkout/views/crypto-intent/methods.js +5 -5
- package/dist/esm/checkout/views/crypto-intent/methods.js.map +1 -1
- package/dist/esm/checkout/views/crypto-intent/wallet-payment-confirmation-amount.js +30 -0
- package/dist/esm/checkout/views/crypto-intent/wallet-payment-confirmation-amount.js.map +1 -0
- package/dist/esm/checkout/views/crypto-intent/wallet-payment-options.js +581 -0
- package/dist/esm/checkout/views/crypto-intent/wallet-payment-options.js.map +1 -0
- package/dist/esm/checkout/views/crypto-intent/wallet-select.js +3 -31
- package/dist/esm/checkout/views/crypto-intent/wallet-select.js.map +1 -1
- package/dist/esm/checkout/views/initial.js +1 -4
- package/dist/esm/checkout/views/initial.js.map +1 -1
- package/dist/esm/checkout/views/restart.js +2 -0
- package/dist/esm/checkout/views/restart.js.map +1 -1
- package/dist/esm/components/text/index.js +1 -0
- package/dist/esm/components/text/index.js.map +1 -1
- package/dist/esm/components/wallet-selector/wallets/wagmi/index.js +8 -1
- package/dist/esm/components/wallet-selector/wallets/wagmi/index.js.map +1 -1
- package/dist/esm/config.js +1 -1
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/integrations/evm-chain.js +22 -0
- package/dist/esm/integrations/evm-chain.js.map +1 -0
- package/dist/esm/integrations/index.js +1 -0
- package/dist/esm/integrations/index.js.map +1 -1
- package/dist/esm/integrations/lifi.js +200 -0
- package/dist/esm/integrations/lifi.js.map +1 -0
- package/dist/esm/state/checkout/payment-attempt.js +43 -0
- package/dist/esm/state/checkout/payment-attempt.js.map +1 -0
- package/dist/esm/state/checkout/payment-lifecycle.js +58 -0
- package/dist/esm/state/checkout/payment-lifecycle.js.map +1 -0
- package/dist/esm/state/checkout/store.js +465 -42
- package/dist/esm/state/checkout/store.js.map +1 -1
- package/dist/types/api/checkouts.d.ts +0 -1
- package/dist/types/api/checkouts.d.ts.map +1 -1
- package/dist/types/api/generated.d.ts +162 -1406
- package/dist/types/api/generated.d.ts.map +1 -1
- package/dist/types/api/payments.d.ts +18 -8
- package/dist/types/api/payments.d.ts.map +1 -1
- package/dist/types/api/types.d.ts +6 -8
- package/dist/types/api/types.d.ts.map +1 -1
- package/dist/types/checkout/index.d.ts.map +1 -1
- package/dist/types/checkout/views/crypto-intent/direct-details.d.ts.map +1 -1
- package/dist/types/checkout/views/crypto-intent/wallet-payment-confirmation-amount.d.ts +13 -0
- package/dist/types/checkout/views/crypto-intent/wallet-payment-confirmation-amount.d.ts.map +1 -0
- package/dist/types/checkout/views/crypto-intent/wallet-payment-options.d.ts +3 -0
- package/dist/types/checkout/views/crypto-intent/wallet-payment-options.d.ts.map +1 -0
- package/dist/types/checkout/views/crypto-intent/wallet-select.d.ts.map +1 -1
- package/dist/types/checkout/views/initial.d.ts.map +1 -1
- package/dist/types/checkout/views/restart.d.ts.map +1 -1
- package/dist/types/components/text/index.d.ts.map +1 -1
- package/dist/types/components/wallet-selector/domain.d.ts +9 -0
- package/dist/types/components/wallet-selector/domain.d.ts.map +1 -1
- package/dist/types/components/wallet-selector/wallets/wagmi/index.d.ts +449 -0
- package/dist/types/components/wallet-selector/wallets/wagmi/index.d.ts.map +1 -1
- package/dist/types/config.d.ts +65 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/integrations/evm-chain.d.ts +3 -0
- package/dist/types/integrations/evm-chain.d.ts.map +1 -0
- package/dist/types/integrations/index.d.ts +1 -0
- package/dist/types/integrations/index.d.ts.map +1 -1
- package/dist/types/integrations/lifi.d.ts +14 -0
- package/dist/types/integrations/lifi.d.ts.map +1 -0
- package/dist/types/state/checkout/payment-attempt.d.ts +9 -0
- package/dist/types/state/checkout/payment-attempt.d.ts.map +1 -0
- package/dist/types/state/checkout/payment-lifecycle.d.ts +20 -0
- package/dist/types/state/checkout/payment-lifecycle.d.ts.map +1 -0
- package/dist/types/state/checkout/store.d.ts +31 -86
- package/dist/types/state/checkout/store.d.ts.map +1 -1
- package/package.json +4 -2
- package/dist/esm/checkout/views/crypto-intent/currency-select.js +0 -44
- package/dist/esm/checkout/views/crypto-intent/currency-select.js.map +0 -1
- package/dist/esm/components/crypto/default-currency-selector.js +0 -62
- package/dist/esm/components/crypto/default-currency-selector.js.map +0 -1
- package/dist/types/checkout/views/crypto-intent/currency-select.d.ts +0 -3
- package/dist/types/checkout/views/crypto-intent/currency-select.d.ts.map +0 -1
- package/dist/types/components/crypto/default-currency-selector.d.ts +0 -9
- package/dist/types/components/crypto/default-currency-selector.d.ts.map +0 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { waitForTransactionReceipt } from "@wagmi/core";
|
|
2
2
|
import { create } from "zustand";
|
|
3
3
|
import { immer } from "zustand/middleware/immer";
|
|
4
|
-
import {
|
|
5
|
-
import { createPaymentAttempt, createTestPaymentAttempt, createFiatPaymentAttempt, getPaymentById, } from "../../api/payments.js";
|
|
4
|
+
import { getCheckoutCryptoCurrencies } from "../../api/checkouts.js";
|
|
5
|
+
import { createPaymentAttempt, createTestPaymentAttempt, createFiatPaymentAttempt, getPaymentById, getRequiredSettlementConfig, } from "../../api/payments.js";
|
|
6
6
|
import { PaymentStatus, } from "../../api/types.js";
|
|
7
7
|
import { WagmiWalletInterface } from "../../components/wallet-selector/wallets/wagmi/index.js";
|
|
8
|
+
import { resolveEvmChainId, switchEvmWalletChain, } from "../../integrations/evm-chain.js";
|
|
9
|
+
import { buildWalletPaymentAttemptPayload } from "./payment-attempt.js";
|
|
10
|
+
import { canCreateFreshAttempt, getAttemptFreshnessStatus, } from "./payment-lifecycle.js";
|
|
8
11
|
export var ACTIONS;
|
|
9
12
|
(function (ACTIONS) {
|
|
10
13
|
ACTIONS["GET_CHECKOUT_CONFIG"] = "GET_CHECKOUT_CONFIG";
|
|
@@ -18,6 +21,20 @@ export var ACTIONS;
|
|
|
18
21
|
ACTIONS["POLL_PAYMENT"] = "POLL_PAYMENT";
|
|
19
22
|
ACTIONS["CHECK_CHAIN_STATUS"] = "CHECK_CHAIN_STATUS";
|
|
20
23
|
})(ACTIONS || (ACTIONS = {}));
|
|
24
|
+
const initialWalletPaymentState = () => ({
|
|
25
|
+
status: "idle",
|
|
26
|
+
options: [],
|
|
27
|
+
walletAddress: null,
|
|
28
|
+
selectedOptionId: null,
|
|
29
|
+
quoteId: null,
|
|
30
|
+
routeId: null,
|
|
31
|
+
sourceTxHash: null,
|
|
32
|
+
destinationTxHash: null,
|
|
33
|
+
quoteStatus: "idle",
|
|
34
|
+
quote: null,
|
|
35
|
+
quoteError: null,
|
|
36
|
+
error: null,
|
|
37
|
+
});
|
|
21
38
|
const initialActionState = (initialValue) => Object.values(ACTIONS).reduce((acc, action) => {
|
|
22
39
|
acc[action] = initialValue;
|
|
23
40
|
return acc;
|
|
@@ -26,6 +43,7 @@ export const useCheckoutStore = create()(immer((_set, _get) => ({
|
|
|
26
43
|
checkoutId: null,
|
|
27
44
|
selectedMethod: null,
|
|
28
45
|
selectedCurrency: null,
|
|
46
|
+
retrySession: false,
|
|
29
47
|
methods: null,
|
|
30
48
|
cryptoCurrency: null,
|
|
31
49
|
checkout: null,
|
|
@@ -46,11 +64,280 @@ export const useCheckoutStore = create()(immer((_set, _get) => ({
|
|
|
46
64
|
pollCount: 0,
|
|
47
65
|
lastPollTime: null,
|
|
48
66
|
},
|
|
67
|
+
attemptFreshness: {
|
|
68
|
+
status: "idle",
|
|
69
|
+
expiresAt: null,
|
|
70
|
+
remainingMs: null,
|
|
71
|
+
},
|
|
72
|
+
attemptRefresh: {
|
|
73
|
+
mode: "none",
|
|
74
|
+
status: "idle",
|
|
75
|
+
error: null,
|
|
76
|
+
},
|
|
77
|
+
walletPayment: initialWalletPaymentState(),
|
|
49
78
|
})));
|
|
50
79
|
const set = (fn) => {
|
|
51
80
|
useCheckoutStore.setState(fn);
|
|
52
81
|
};
|
|
53
82
|
const get = () => useCheckoutStore.getState();
|
|
83
|
+
let attemptFreshnessTimer = null;
|
|
84
|
+
let attemptFreshnessInterval = null;
|
|
85
|
+
const clearAttemptFreshnessTimer = () => {
|
|
86
|
+
if (attemptFreshnessTimer) {
|
|
87
|
+
clearTimeout(attemptFreshnessTimer);
|
|
88
|
+
attemptFreshnessTimer = null;
|
|
89
|
+
}
|
|
90
|
+
if (attemptFreshnessInterval) {
|
|
91
|
+
clearInterval(attemptFreshnessInterval);
|
|
92
|
+
attemptFreshnessInterval = null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const buildDirectWalletQuote = (option, attempt) => {
|
|
96
|
+
const amount = String(attempt.amount_formatted);
|
|
97
|
+
return {
|
|
98
|
+
quoteId: option.quoteId,
|
|
99
|
+
routeId: option.routeId,
|
|
100
|
+
toolName: "Direct transfer",
|
|
101
|
+
fromAmount: amount,
|
|
102
|
+
fromAmountDisplay: amount,
|
|
103
|
+
toAmount: amount,
|
|
104
|
+
toAmountDisplay: amount,
|
|
105
|
+
estimatedDurationMinutes: 1,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const setAttemptRefreshState = (update, walletUpdate) => {
|
|
109
|
+
set((state) => {
|
|
110
|
+
Object.assign(state.attemptRefresh, update);
|
|
111
|
+
if (walletUpdate) {
|
|
112
|
+
Object.assign(state.walletPayment, walletUpdate);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const getWalletQuoteLoadingState = () => ({
|
|
117
|
+
quoteStatus: "loading",
|
|
118
|
+
quote: null,
|
|
119
|
+
quoteError: null,
|
|
120
|
+
quoteId: null,
|
|
121
|
+
routeId: null,
|
|
122
|
+
});
|
|
123
|
+
const getWalletQuoteReadyState = (quote) => ({
|
|
124
|
+
quoteStatus: "ready",
|
|
125
|
+
quote,
|
|
126
|
+
quoteId: quote.quoteId,
|
|
127
|
+
routeId: quote.routeId,
|
|
128
|
+
quoteError: null,
|
|
129
|
+
});
|
|
130
|
+
const getWalletQuoteFailedState = (message) => ({
|
|
131
|
+
quoteStatus: "failed",
|
|
132
|
+
quote: null,
|
|
133
|
+
quoteError: message,
|
|
134
|
+
});
|
|
135
|
+
const beginAttemptRefresh = (walletUpdate) => {
|
|
136
|
+
setAttemptRefreshState({
|
|
137
|
+
status: "refreshing",
|
|
138
|
+
error: null,
|
|
139
|
+
}, walletUpdate);
|
|
140
|
+
};
|
|
141
|
+
const finishAttemptRefresh = (walletUpdate) => {
|
|
142
|
+
setAttemptRefreshState({
|
|
143
|
+
status: "idle",
|
|
144
|
+
error: null,
|
|
145
|
+
}, walletUpdate);
|
|
146
|
+
};
|
|
147
|
+
const failAttemptRefresh = (message) => {
|
|
148
|
+
const { attemptRefresh } = get();
|
|
149
|
+
setAttemptRefreshState({
|
|
150
|
+
status: "failed",
|
|
151
|
+
error: message,
|
|
152
|
+
}, attemptRefresh.mode === "wallet"
|
|
153
|
+
? getWalletQuoteFailedState(message)
|
|
154
|
+
: undefined);
|
|
155
|
+
};
|
|
156
|
+
const getWalletAttemptRefreshContext = () => {
|
|
157
|
+
const state = get();
|
|
158
|
+
const payment = state.payment;
|
|
159
|
+
const checkout = state.checkout;
|
|
160
|
+
const settlementCurrency = state.cryptoCurrency;
|
|
161
|
+
const selectedOption = state.walletPayment.options.find((option) => option.id === state.walletPayment.selectedOptionId);
|
|
162
|
+
const walletAddress = state.walletPayment.walletAddress;
|
|
163
|
+
if (!payment || !checkout || !settlementCurrency) {
|
|
164
|
+
throw new Error("Wallet payment context is not ready");
|
|
165
|
+
}
|
|
166
|
+
if (!selectedOption || !walletAddress) {
|
|
167
|
+
throw new Error("Missing wallet payment selection for quote refresh");
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
payment,
|
|
171
|
+
checkout,
|
|
172
|
+
settlementCurrency,
|
|
173
|
+
selectedOption,
|
|
174
|
+
walletAddress,
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const resolveRefreshedWalletQuote = async ({ selectedOption, checkout, payment, settlementCurrency, walletAddress, attempt, }) => {
|
|
178
|
+
if (selectedOption.kind === "direct") {
|
|
179
|
+
return buildDirectWalletQuote(selectedOption, attempt);
|
|
180
|
+
}
|
|
181
|
+
// Keep routed-quote loading behind a dynamic import so the checkout store
|
|
182
|
+
// does not eagerly pull the LI.FI integration path into manual-only flows.
|
|
183
|
+
const { fetchWalletPaymentQuote } = await import("../../integrations/index.js");
|
|
184
|
+
return fetchWalletPaymentQuote({
|
|
185
|
+
option: selectedOption,
|
|
186
|
+
context: {
|
|
187
|
+
checkout,
|
|
188
|
+
payment: get().payment ?? payment,
|
|
189
|
+
settlementCurrency,
|
|
190
|
+
walletAddress,
|
|
191
|
+
cryptoPaymentAttempt: attempt,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
const refreshWalletPaymentAttempt = async () => {
|
|
196
|
+
const context = getWalletAttemptRefreshContext();
|
|
197
|
+
beginAttemptRefresh(getWalletQuoteLoadingState());
|
|
198
|
+
const attempt = await createWalletOptionPaymentAttempt(context.selectedOption, context.walletAddress, null);
|
|
199
|
+
const quote = await resolveRefreshedWalletQuote({
|
|
200
|
+
...context,
|
|
201
|
+
attempt,
|
|
202
|
+
});
|
|
203
|
+
finishAttemptRefresh(getWalletQuoteReadyState(quote));
|
|
204
|
+
};
|
|
205
|
+
const refreshCurrentAttempt = async () => {
|
|
206
|
+
const state = get();
|
|
207
|
+
if (state.attemptRefresh.status === "refreshing") {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!canCreateFreshAttempt(state.payment)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
if (state.attemptRefresh.mode === "wallet") {
|
|
215
|
+
await refreshWalletPaymentAttempt();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (state.attemptRefresh.mode === "manual") {
|
|
219
|
+
beginAttemptRefresh();
|
|
220
|
+
await createManualDirectPaymentAttempt();
|
|
221
|
+
finishAttemptRefresh();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const message = error instanceof Error
|
|
226
|
+
? error.message
|
|
227
|
+
: "Failed to refresh payment quote";
|
|
228
|
+
failAttemptRefresh(message);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const scheduleAttemptFreshness = (payment, attempt) => {
|
|
232
|
+
clearAttemptFreshnessTimer();
|
|
233
|
+
const freshness = getAttemptFreshnessStatus(payment, attempt ?? null);
|
|
234
|
+
set((state) => {
|
|
235
|
+
state.attemptFreshness = freshness;
|
|
236
|
+
});
|
|
237
|
+
if (freshness.status !== "fresh" || !freshness.expiresAt) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// The one-shot timer is the authoritative expiry transition. When the
|
|
241
|
+
// current attempt crosses its `expires_at`, we recompute freshness against the
|
|
242
|
+
// latest payment snapshot and kick off a single refresh if the attempt is now
|
|
243
|
+
// stale.
|
|
244
|
+
const delay = Math.max(0, freshness.expiresAt - Date.now());
|
|
245
|
+
attemptFreshnessTimer = setTimeout(() => {
|
|
246
|
+
const state = get();
|
|
247
|
+
const currentAttempt = state.cryptoPaymentAttempt ?? state.payment?.payment_attempt;
|
|
248
|
+
const currentFreshness = getAttemptFreshnessStatus(state.payment, currentAttempt);
|
|
249
|
+
set((draft) => {
|
|
250
|
+
draft.attemptFreshness = currentFreshness;
|
|
251
|
+
});
|
|
252
|
+
if (currentFreshness.status === "stale") {
|
|
253
|
+
void refreshCurrentAttempt();
|
|
254
|
+
}
|
|
255
|
+
}, delay);
|
|
256
|
+
// The interval is only for UI countdown updates while an attempt is still
|
|
257
|
+
// fresh. It keeps `remainingMs` moving without waiting for the expiry timer to
|
|
258
|
+
// fire, and stops itself once the attempt is no longer usable.
|
|
259
|
+
attemptFreshnessInterval = setInterval(() => {
|
|
260
|
+
const currentState = get();
|
|
261
|
+
const currentAttempt = currentState.cryptoPaymentAttempt ??
|
|
262
|
+
currentState.payment?.payment_attempt;
|
|
263
|
+
const currentFreshness = getAttemptFreshnessStatus(currentState.payment, currentAttempt);
|
|
264
|
+
if (currentFreshness.status !== "fresh" || !currentFreshness.expiresAt) {
|
|
265
|
+
clearAttemptFreshnessTimer();
|
|
266
|
+
set((state) => {
|
|
267
|
+
state.attemptFreshness = {
|
|
268
|
+
status: currentFreshness.status,
|
|
269
|
+
expiresAt: currentFreshness.expiresAt,
|
|
270
|
+
remainingMs: currentFreshness.expiresAt !== null
|
|
271
|
+
? 0
|
|
272
|
+
: currentFreshness.remainingMs,
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
set((state) => {
|
|
278
|
+
state.attemptFreshness = {
|
|
279
|
+
...currentFreshness,
|
|
280
|
+
remainingMs: currentFreshness.remainingMs,
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}, 1000);
|
|
284
|
+
};
|
|
285
|
+
export const syncCheckoutAttemptFreshness = (payment, attempt) => {
|
|
286
|
+
// Re-sync the freshness timers whenever the payment snapshot or active
|
|
287
|
+
// attempt changes. This is the single entrypoint used after loading a
|
|
288
|
+
// payment, creating a fresh attempt, or resetting session state.
|
|
289
|
+
scheduleAttemptFreshness(payment, attempt);
|
|
290
|
+
};
|
|
291
|
+
export const configureWalletAttemptRefresh = (walletAddress) => {
|
|
292
|
+
set((state) => {
|
|
293
|
+
// Wallet refresh mode means stale attempts should be replaced by creating a
|
|
294
|
+
// fresh crypto payment attempt and reloading wallet quote state for the
|
|
295
|
+
// currently selected wallet route.
|
|
296
|
+
state.attemptRefresh.mode = "wallet";
|
|
297
|
+
state.attemptRefresh.error = null;
|
|
298
|
+
if (state.attemptRefresh.status !== "refreshing") {
|
|
299
|
+
state.attemptRefresh.status = "idle";
|
|
300
|
+
}
|
|
301
|
+
state.walletPayment.walletAddress = walletAddress;
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
export const configureManualAttemptRefresh = () => {
|
|
305
|
+
set((state) => {
|
|
306
|
+
// Manual refresh mode reuses the same stale-attempt lifecycle, but the
|
|
307
|
+
// refresh path only needs a new direct/manual attempt and not any wallet
|
|
308
|
+
// quote metadata.
|
|
309
|
+
state.attemptRefresh.mode = "manual";
|
|
310
|
+
state.attemptRefresh.error = null;
|
|
311
|
+
if (state.attemptRefresh.status !== "refreshing") {
|
|
312
|
+
state.attemptRefresh.status = "idle";
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
export const clearAttemptRefreshContext = () => {
|
|
317
|
+
set((state) => {
|
|
318
|
+
// Clear any stale refresh mode before returning to method selection or
|
|
319
|
+
// closing the checkout so future expiry events do not try to reuse old
|
|
320
|
+
// wallet/manual context.
|
|
321
|
+
state.attemptRefresh = {
|
|
322
|
+
mode: "none",
|
|
323
|
+
status: "idle",
|
|
324
|
+
error: null,
|
|
325
|
+
};
|
|
326
|
+
state.walletPayment.walletAddress = null;
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
const updatePaymentSnapshot = (payment, attempt, extra) => {
|
|
330
|
+
set((state) => {
|
|
331
|
+
state.payment = payment;
|
|
332
|
+
if (attempt !== undefined) {
|
|
333
|
+
state.cryptoPaymentAttempt = attempt;
|
|
334
|
+
}
|
|
335
|
+
if (extra) {
|
|
336
|
+
extra(state);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
syncCheckoutAttemptFreshness(payment, attempt ?? payment?.payment_attempt);
|
|
340
|
+
};
|
|
54
341
|
export const clearErrors = () => {
|
|
55
342
|
set((state) => {
|
|
56
343
|
Object.values(ACTIONS).forEach((action) => {
|
|
@@ -59,11 +346,12 @@ export const clearErrors = () => {
|
|
|
59
346
|
});
|
|
60
347
|
};
|
|
61
348
|
export const initialiseStore = async (payment) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
349
|
+
resetCheckoutStore();
|
|
350
|
+
updatePaymentSnapshot(payment);
|
|
65
351
|
try {
|
|
66
|
-
await
|
|
352
|
+
const refreshedPayment = await getPaymentById(payment.id);
|
|
353
|
+
updatePaymentSnapshot(refreshedPayment);
|
|
354
|
+
hydrateCheckoutFromPayment(refreshedPayment);
|
|
67
355
|
}
|
|
68
356
|
catch (err) {
|
|
69
357
|
set((state) => {
|
|
@@ -71,24 +359,39 @@ export const initialiseStore = async (payment) => {
|
|
|
71
359
|
});
|
|
72
360
|
}
|
|
73
361
|
};
|
|
74
|
-
export const
|
|
75
|
-
|
|
362
|
+
export const createWalletOptionPaymentAttempt = async (
|
|
363
|
+
// "direct" is a routing classification: the payer sends the settlement
|
|
364
|
+
// asset on the settlement chain. It is not tied to manual vs wallet UX.
|
|
365
|
+
option, sendingAddress, quote) => {
|
|
366
|
+
const payment = useCheckoutStore.getState().payment;
|
|
367
|
+
const paymentId = payment?.id;
|
|
76
368
|
if (!paymentId) {
|
|
77
369
|
throw Error("Cannot create payment attempt without payment object.");
|
|
78
370
|
}
|
|
371
|
+
if (!canCreateFreshAttempt(payment)) {
|
|
372
|
+
throw Error("This payment is no longer payable.");
|
|
373
|
+
}
|
|
79
374
|
try {
|
|
80
375
|
set((state) => {
|
|
81
376
|
state.error[ACTIONS.CREATE_PAYMENT_ATTEMPT] = null;
|
|
82
377
|
state.loading[ACTIONS.CREATE_PAYMENT_ATTEMPT] = true;
|
|
83
378
|
});
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
379
|
+
const attemptPayload = buildWalletPaymentAttemptPayload({
|
|
380
|
+
option,
|
|
381
|
+
sendingAddress,
|
|
382
|
+
quote,
|
|
383
|
+
requiredConfirmations: 1,
|
|
384
|
+
});
|
|
385
|
+
const attempt = await createPaymentAttempt(paymentId, attemptPayload, 1);
|
|
386
|
+
const refreshedPayment = await getPaymentById(paymentId);
|
|
387
|
+
updatePaymentSnapshot(refreshedPayment, attempt, (state) => {
|
|
388
|
+
state.retrySession = false;
|
|
389
|
+
state.attemptRefresh.status = "idle";
|
|
390
|
+
state.attemptRefresh.error = null;
|
|
89
391
|
state.error[ACTIONS.CREATE_PAYMENT_ATTEMPT] = null;
|
|
90
392
|
state.loading[ACTIONS.CREATE_PAYMENT_ATTEMPT] = false;
|
|
91
393
|
});
|
|
394
|
+
return attempt;
|
|
92
395
|
}
|
|
93
396
|
catch (err) {
|
|
94
397
|
const error = err;
|
|
@@ -96,8 +399,35 @@ export const createCheckoutCryptoPaymentAttempt = async (currency) => {
|
|
|
96
399
|
state.error[ACTIONS.CREATE_PAYMENT_ATTEMPT] = error.message;
|
|
97
400
|
state.loading[ACTIONS.CREATE_PAYMENT_ATTEMPT] = false;
|
|
98
401
|
});
|
|
402
|
+
throw error;
|
|
99
403
|
}
|
|
100
404
|
};
|
|
405
|
+
export const createManualDirectPaymentAttempt = async (sendingAddress = "", confirmations = 1) => {
|
|
406
|
+
const payment = useCheckoutStore.getState().payment;
|
|
407
|
+
const paymentId = payment?.id;
|
|
408
|
+
if (!paymentId) {
|
|
409
|
+
throw Error("Cannot create payment attempt without payment object.");
|
|
410
|
+
}
|
|
411
|
+
const settlementConfig = getRequiredSettlementConfig(payment);
|
|
412
|
+
const attempt = await createPaymentAttempt(paymentId, {
|
|
413
|
+
sendingAddress,
|
|
414
|
+
required_confirmations: confirmations,
|
|
415
|
+
routing: {
|
|
416
|
+
type: "direct",
|
|
417
|
+
source: {
|
|
418
|
+
token: settlementConfig.asset,
|
|
419
|
+
chain: settlementConfig.chain,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
const refreshedPayment = await getPaymentById(paymentId);
|
|
424
|
+
updatePaymentSnapshot(refreshedPayment, attempt, (state) => {
|
|
425
|
+
state.retrySession = false;
|
|
426
|
+
state.attemptRefresh.status = "idle";
|
|
427
|
+
state.attemptRefresh.error = null;
|
|
428
|
+
});
|
|
429
|
+
return attempt;
|
|
430
|
+
};
|
|
101
431
|
export const createSuccessfulTestPaymentAttempt = async () => {
|
|
102
432
|
const paymentId = useCheckoutStore.getState().payment?.id;
|
|
103
433
|
if (!paymentId) {
|
|
@@ -108,11 +438,14 @@ export const createSuccessfulTestPaymentAttempt = async () => {
|
|
|
108
438
|
state.error[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = null;
|
|
109
439
|
state.loading[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = true;
|
|
110
440
|
});
|
|
111
|
-
const
|
|
441
|
+
const currentPayment = useCheckoutStore.getState().payment;
|
|
442
|
+
if (!currentPayment) {
|
|
443
|
+
throw Error("Cannot create test payment attempt without payment object.");
|
|
444
|
+
}
|
|
445
|
+
const attempt = await createTestPaymentAttempt(paymentId, "0xsuccess", currentPayment);
|
|
112
446
|
const payment = await getPaymentById(paymentId);
|
|
113
|
-
|
|
114
|
-
state.
|
|
115
|
-
state.cryptoPaymentAttempt = attempt;
|
|
447
|
+
updatePaymentSnapshot(payment, attempt, (state) => {
|
|
448
|
+
state.retrySession = false;
|
|
116
449
|
state.error[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = null;
|
|
117
450
|
state.loading[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = false;
|
|
118
451
|
});
|
|
@@ -136,11 +469,14 @@ export const createFailedTestPaymentAttempt = async () => {
|
|
|
136
469
|
state.error[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = null;
|
|
137
470
|
state.loading[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = true;
|
|
138
471
|
});
|
|
139
|
-
const
|
|
472
|
+
const currentPayment = useCheckoutStore.getState().payment;
|
|
473
|
+
if (!currentPayment) {
|
|
474
|
+
throw Error("Cannot create test payment attempt without payment object.");
|
|
475
|
+
}
|
|
476
|
+
const attempt = await createTestPaymentAttempt(paymentId, "0xfail", currentPayment);
|
|
140
477
|
const payment = await getPaymentById(paymentId);
|
|
141
|
-
|
|
142
|
-
state.
|
|
143
|
-
state.cryptoPaymentAttempt = attempt;
|
|
478
|
+
updatePaymentSnapshot(payment, attempt, (state) => {
|
|
479
|
+
state.retrySession = false;
|
|
144
480
|
state.error[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = null;
|
|
145
481
|
state.loading[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = false;
|
|
146
482
|
});
|
|
@@ -188,33 +524,20 @@ export function getAvailableCheckoutMethods(checkout) {
|
|
|
188
524
|
intentName: "cryptoIntent",
|
|
189
525
|
});
|
|
190
526
|
}
|
|
191
|
-
if (checkout.checkout_config.card?.enabled) {
|
|
192
|
-
methods.push({
|
|
193
|
-
name: "Pay by Card",
|
|
194
|
-
intentName: "cardIntent",
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
if (checkout.checkout_config.bank?.enabled) {
|
|
198
|
-
methods.push({
|
|
199
|
-
name: "Pay by Bank",
|
|
200
|
-
intentName: "bankIntent",
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
527
|
return methods;
|
|
204
528
|
}
|
|
205
|
-
export const
|
|
529
|
+
export const hydrateCheckoutFromPayment = (payment) => {
|
|
206
530
|
const ACTION = ACTIONS.GET_CHECKOUT_CONFIG;
|
|
207
531
|
set((state) => {
|
|
208
532
|
state.loading[ACTION] = true;
|
|
209
533
|
state.error[ACTION] = null;
|
|
210
534
|
});
|
|
211
535
|
try {
|
|
212
|
-
|
|
213
|
-
if (resp) {
|
|
536
|
+
if (payment.checkout) {
|
|
214
537
|
set((state) => {
|
|
215
|
-
state.checkout =
|
|
216
|
-
state.methods = getAvailableCheckoutMethods(
|
|
217
|
-
state.cryptoCurrency = getCheckoutCryptoCurrencies(
|
|
538
|
+
state.checkout = payment.checkout;
|
|
539
|
+
state.methods = getAvailableCheckoutMethods(payment.checkout);
|
|
540
|
+
state.cryptoCurrency = getCheckoutCryptoCurrencies(payment.checkout);
|
|
218
541
|
state.loading[ACTION] = false;
|
|
219
542
|
state.error[ACTION] = null;
|
|
220
543
|
});
|
|
@@ -250,11 +573,12 @@ export const getPayment = async () => {
|
|
|
250
573
|
try {
|
|
251
574
|
const resp = await getPaymentById(payment.id);
|
|
252
575
|
if (resp) {
|
|
576
|
+
updatePaymentSnapshot(resp);
|
|
253
577
|
set((state) => {
|
|
254
|
-
state.payment = resp;
|
|
255
578
|
state.loading[ACTION] = false;
|
|
256
579
|
state.error[ACTION] = null;
|
|
257
580
|
});
|
|
581
|
+
hydrateCheckoutFromPayment(resp);
|
|
258
582
|
return resp;
|
|
259
583
|
}
|
|
260
584
|
else {
|
|
@@ -277,7 +601,7 @@ export const executeTransfer = async (wallet) => {
|
|
|
277
601
|
const ACTION = ACTIONS.EXECUTE_TRANSFER;
|
|
278
602
|
const state = get();
|
|
279
603
|
console.log("[Execute Transfer] State:", state);
|
|
280
|
-
if (!state.cryptoPaymentAttempt
|
|
604
|
+
if (!state.cryptoPaymentAttempt) {
|
|
281
605
|
throw new Error("Missing required data for payment execution");
|
|
282
606
|
}
|
|
283
607
|
set((state) => {
|
|
@@ -287,6 +611,14 @@ export const executeTransfer = async (wallet) => {
|
|
|
287
611
|
state.transfer.error = null;
|
|
288
612
|
});
|
|
289
613
|
try {
|
|
614
|
+
const settlementChainHex = state.cryptoPaymentAttempt.settlement_config.chain_hex;
|
|
615
|
+
const targetChainId = resolveEvmChainId(settlementChainHex);
|
|
616
|
+
if (targetChainId && wallet.switchChain) {
|
|
617
|
+
await wallet.switchChain(targetChainId);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
await switchEvmWalletChain(settlementChainHex);
|
|
621
|
+
}
|
|
290
622
|
const address = await wallet.getAddress();
|
|
291
623
|
if (!address) {
|
|
292
624
|
throw new Error("Failed to get wallet address");
|
|
@@ -297,7 +629,7 @@ export const executeTransfer = async (wallet) => {
|
|
|
297
629
|
amount: Number(state.cryptoPaymentAttempt.amount),
|
|
298
630
|
evm: {
|
|
299
631
|
implementationAddress: state.cryptoPaymentAttempt.settlement_token_address,
|
|
300
|
-
assetName: state.
|
|
632
|
+
assetName: state.cryptoPaymentAttempt.settlement_config.asset_name,
|
|
301
633
|
},
|
|
302
634
|
};
|
|
303
635
|
const txHash = await wallet.executeTransfer(transfer);
|
|
@@ -373,6 +705,25 @@ export const resetTransfer = () => {
|
|
|
373
705
|
};
|
|
374
706
|
});
|
|
375
707
|
};
|
|
708
|
+
export const executeConnectedWalletPayment = async (wallet) => {
|
|
709
|
+
await executeTransfer(wallet);
|
|
710
|
+
void checkChainStatus(wallet);
|
|
711
|
+
await waitForTransferSuccess();
|
|
712
|
+
};
|
|
713
|
+
function waitForTransferSuccess() {
|
|
714
|
+
return new Promise((resolve, reject) => {
|
|
715
|
+
const unsubscribe = useCheckoutStore.subscribe((state) => {
|
|
716
|
+
if (state.transfer.chainStatus === "confirmed") {
|
|
717
|
+
unsubscribe();
|
|
718
|
+
resolve();
|
|
719
|
+
}
|
|
720
|
+
else if (state.transfer.chainStatus === "failed") {
|
|
721
|
+
unsubscribe();
|
|
722
|
+
reject(new Error(state.transfer.error || "Transaction failed on chain"));
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
}
|
|
376
727
|
export const startPaymentPolling = (onPaymentStatusChange, confirmationStatus = PaymentStatus.PAID) => {
|
|
377
728
|
const ACTION = ACTIONS.POLL_PAYMENT;
|
|
378
729
|
const POLL_INTERVAL = 5000;
|
|
@@ -439,17 +790,29 @@ export const stopPaymentPolling = () => {
|
|
|
439
790
|
});
|
|
440
791
|
};
|
|
441
792
|
export const resetCheckoutStore = () => {
|
|
793
|
+
clearAttemptFreshnessTimer();
|
|
442
794
|
set((state) => {
|
|
443
795
|
Object.assign(state, {
|
|
444
796
|
amount: null,
|
|
445
797
|
checkoutId: null,
|
|
446
798
|
selectedMethod: null,
|
|
447
799
|
selectedCurrency: null,
|
|
800
|
+
retrySession: false,
|
|
448
801
|
methods: null,
|
|
449
802
|
cryptoCurrency: null,
|
|
450
803
|
checkout: null,
|
|
451
804
|
payment: null,
|
|
452
805
|
cryptoPaymentAttempt: null,
|
|
806
|
+
attemptFreshness: {
|
|
807
|
+
status: "idle",
|
|
808
|
+
expiresAt: null,
|
|
809
|
+
remainingMs: null,
|
|
810
|
+
},
|
|
811
|
+
attemptRefresh: {
|
|
812
|
+
mode: "none",
|
|
813
|
+
status: "idle",
|
|
814
|
+
error: null,
|
|
815
|
+
},
|
|
453
816
|
loading: initialActionState(false),
|
|
454
817
|
error: initialActionState(null),
|
|
455
818
|
transfer: {
|
|
@@ -463,9 +826,59 @@ export const resetCheckoutStore = () => {
|
|
|
463
826
|
pollCount: 0,
|
|
464
827
|
lastPollTime: null,
|
|
465
828
|
},
|
|
829
|
+
walletPayment: initialWalletPaymentState(),
|
|
466
830
|
});
|
|
467
831
|
});
|
|
468
832
|
};
|
|
833
|
+
export const resetCheckoutSessionForRetry = () => {
|
|
834
|
+
clearAttemptFreshnessTimer();
|
|
835
|
+
set((state) => {
|
|
836
|
+
// Retry keeps the same payment intent but discards all attempt-scoped UI
|
|
837
|
+
// state so the payer can choose a fresh route. The payment object itself is
|
|
838
|
+
// preserved elsewhere in the store; this reset only clears execution state
|
|
839
|
+
// that was tied to the previous attempt.
|
|
840
|
+
state.selectedMethod = null;
|
|
841
|
+
state.selectedCurrency = null;
|
|
842
|
+
state.retrySession = true;
|
|
843
|
+
state.cryptoPaymentAttempt = null;
|
|
844
|
+
state.fiatPaymentAttempt = null;
|
|
845
|
+
state.processing = false;
|
|
846
|
+
state.transfer = {
|
|
847
|
+
status: "idle",
|
|
848
|
+
error: null,
|
|
849
|
+
transactionHash: null,
|
|
850
|
+
chainStatus: "pending",
|
|
851
|
+
};
|
|
852
|
+
state.polling = {
|
|
853
|
+
isPolling: false,
|
|
854
|
+
pollCount: 0,
|
|
855
|
+
lastPollTime: null,
|
|
856
|
+
};
|
|
857
|
+
state.walletPayment = initialWalletPaymentState();
|
|
858
|
+
state.attemptFreshness = {
|
|
859
|
+
status: "idle",
|
|
860
|
+
expiresAt: null,
|
|
861
|
+
remainingMs: null,
|
|
862
|
+
};
|
|
863
|
+
state.attemptRefresh = {
|
|
864
|
+
mode: "none",
|
|
865
|
+
status: "idle",
|
|
866
|
+
error: null,
|
|
867
|
+
};
|
|
868
|
+
state.error[ACTIONS.CREATE_PAYMENT_ATTEMPT] = null;
|
|
869
|
+
state.error[ACTIONS.CREATE_FIAT_PAYMENT_ATTEMPT] = null;
|
|
870
|
+
state.error[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = null;
|
|
871
|
+
state.error[ACTIONS.EXECUTE_TRANSFER] = null;
|
|
872
|
+
state.error[ACTIONS.POLL_PAYMENT] = null;
|
|
873
|
+
state.error[ACTIONS.CHECK_CHAIN_STATUS] = null;
|
|
874
|
+
state.loading[ACTIONS.CREATE_PAYMENT_ATTEMPT] = false;
|
|
875
|
+
state.loading[ACTIONS.CREATE_FIAT_PAYMENT_ATTEMPT] = false;
|
|
876
|
+
state.loading[ACTIONS.CREATE_TEST_PAYMENT_ATTEMPT] = false;
|
|
877
|
+
state.loading[ACTIONS.EXECUTE_TRANSFER] = false;
|
|
878
|
+
state.loading[ACTIONS.POLL_PAYMENT] = false;
|
|
879
|
+
state.loading[ACTIONS.CHECK_CHAIN_STATUS] = false;
|
|
880
|
+
});
|
|
881
|
+
};
|
|
469
882
|
export const setCheckoutMethod = (method) => {
|
|
470
883
|
set((state) => {
|
|
471
884
|
state.selectedMethod = method;
|
|
@@ -491,4 +904,14 @@ export const isTestMode = () => {
|
|
|
491
904
|
}
|
|
492
905
|
return false;
|
|
493
906
|
};
|
|
907
|
+
export const setWalletPaymentState = (update) => {
|
|
908
|
+
set((state) => {
|
|
909
|
+
Object.assign(state.walletPayment, update);
|
|
910
|
+
});
|
|
911
|
+
};
|
|
912
|
+
export const resetWalletPaymentState = () => {
|
|
913
|
+
set((state) => {
|
|
914
|
+
state.walletPayment = initialWalletPaymentState();
|
|
915
|
+
});
|
|
916
|
+
};
|
|
494
917
|
//# sourceMappingURL=store.js.map
|