@heyama/pay-react-native 1.0.7 → 1.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.
- package/dist/checkout-core/hooks/use-checkout-controller.d.ts +1 -1
- package/dist/checkout-core/hooks/use-checkout-controller.js +28 -5
- package/dist/checkout-core/hooks/use-checkout-payment.d.ts +9 -0
- package/dist/checkout-core/hooks/use-checkout-payment.js +99 -0
- package/dist/checkout-core/models/checkout.interface.d.ts +43 -1
- package/dist/components/BrissotoPayProvider.d.ts +3 -2
- package/dist/components/BrissotoPayProvider.js +7 -13
- package/dist/components/PaymentSheetModal.d.ts +1 -1
- package/dist/components/PaymentSheetModal.js +6 -6
- package/dist/components/SheetContent.d.ts +1 -1
- package/dist/components/SheetContent.js +4 -4
- package/dist/hooks/use-native-platform.js +9 -0
- package/dist/models/sheet.interface.d.ts +3 -19
- package/dist/services/checkout.service.d.ts +6 -5
- package/dist/services/checkout.service.js +17 -20
- package/package.json +1 -1
- package/dist/hooks/use-watched-payment.d.ts +0 -6
- package/dist/hooks/use-watched-payment.js +0 -17
|
@@ -6,4 +6,4 @@ import type { CheckoutControllerOptions, CheckoutCore, CheckoutView } from '../m
|
|
|
6
6
|
* clipboard, leaving for the app, the card fields page) comes in `platform`.
|
|
7
7
|
* The language is not here: it comes from the IntlProvider around it.
|
|
8
8
|
*/
|
|
9
|
-
export declare function useCheckoutController(view: CheckoutView | null | undefined, { clues,
|
|
9
|
+
export declare function useCheckoutController(view: CheckoutView | null | undefined, { clues, calls, platform }: CheckoutControllerOptions): CheckoutCore;
|
|
@@ -31,8 +31,9 @@ const NO_PAYMENT_UI = {
|
|
|
31
31
|
* clipboard, leaving for the app, the card fields page) comes in `platform`.
|
|
32
32
|
* The language is not here: it comes from the IntlProvider around it.
|
|
33
33
|
*/
|
|
34
|
-
export function useCheckoutController(view, { clues,
|
|
34
|
+
export function useCheckoutController(view, { clues, calls, platform }) {
|
|
35
35
|
const t = useTranslations('checkout');
|
|
36
|
+
const mutations = calls?.mutations ?? null;
|
|
36
37
|
const format = useFormatter();
|
|
37
38
|
const lang = useLocale();
|
|
38
39
|
const [patch, setPatch] = useState({});
|
|
@@ -131,12 +132,33 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
131
132
|
opened.current = paymentId;
|
|
132
133
|
void mutations.open({ paymentId });
|
|
133
134
|
}, [paymentId, mutations]);
|
|
134
|
-
//
|
|
135
|
+
// Away from the page or the app while waiting, the payment is not followed
|
|
136
|
+
// live. Coming back asks the provider now (ADR 0001), then follows again.
|
|
137
|
+
const [away, setAway] = useState(false);
|
|
135
138
|
useEffect(() => {
|
|
136
139
|
if (!waiting || !paymentId || !mutations)
|
|
137
140
|
return undefined;
|
|
138
|
-
|
|
141
|
+
// Already away when the wait starts (off to the dialler): not followed until back.
|
|
142
|
+
if (platform.isAway())
|
|
143
|
+
setAway(true);
|
|
144
|
+
const stopLeave = platform.onLeave(() => setAway(true));
|
|
145
|
+
const stopReturn = platform.onReturn(() => void mutations
|
|
146
|
+
.checkNow({ paymentId })
|
|
147
|
+
.catch(() => null)
|
|
148
|
+
.then(() => setAway(false)));
|
|
149
|
+
return () => {
|
|
150
|
+
stopLeave();
|
|
151
|
+
stopReturn();
|
|
152
|
+
setAway(false);
|
|
153
|
+
};
|
|
139
154
|
}, [waiting, paymentId, mutations, platform]);
|
|
155
|
+
// Followed live only on the waiting screen (ADR 0006).
|
|
156
|
+
const followed = waiting && !away;
|
|
157
|
+
const follow = calls?.follow;
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
follow?.(followed);
|
|
160
|
+
}, [follow, followed]);
|
|
161
|
+
const refresh = calls?.refresh;
|
|
140
162
|
const goBack = useCallback(() => update({ redirecting: true }), [update]);
|
|
141
163
|
const countdown = useReturnCountdown(screen === CheckoutScreenEnum.SUCCESS, goBack);
|
|
142
164
|
const returnUrl = view?.returnUrl;
|
|
@@ -161,9 +183,10 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
161
183
|
...(message.complete ? {} : { cardPaying: false }),
|
|
162
184
|
});
|
|
163
185
|
break;
|
|
164
|
-
// The card is with its provider: the waiting screen follows the checks.
|
|
186
|
+
// The card is with its provider: read the attempt, the waiting screen follows the checks.
|
|
165
187
|
case CardFieldsMessageEnum.DONE:
|
|
166
188
|
update({ cardPaying: false });
|
|
189
|
+
refresh?.();
|
|
167
190
|
break;
|
|
168
191
|
case CardFieldsMessageEnum.REFUSED:
|
|
169
192
|
update({ cardPaying: false });
|
|
@@ -172,7 +195,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
172
195
|
}
|
|
173
196
|
};
|
|
174
197
|
return platform.onCardMessage(onMessage);
|
|
175
|
-
}, [update, showToast, refusals, failedToast, platform]);
|
|
198
|
+
}, [update, showToast, refusals, failedToast, platform, refresh]);
|
|
176
199
|
/* ---------------------------------------------------------- helpers */
|
|
177
200
|
const reference = attempt?.reference ?? view?.id ?? '';
|
|
178
201
|
const comingSoon = (name) => toast.show(t('toast.soon', { method: name }), CheckoutToastToneEnum.INFO);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CheckoutConnection, CheckoutPayment } from '../models/checkout.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* One payment for a checkout (ADR 0006): read once over plain calls, read
|
|
4
|
+
* again after each call that changes it, and followed live only between
|
|
5
|
+
* `follow(true)` and `follow(false)`, which the controller says while the
|
|
6
|
+
* payer is on the waiting screen. Once `paymentId` is cleared (a sheet
|
|
7
|
+
* closing) the last answer stays.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useCheckoutPayment(connection: CheckoutConnection, paymentId: string | null): CheckoutPayment;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* One payment for a checkout (ADR 0006): read once over plain calls, read
|
|
4
|
+
* again after each call that changes it, and followed live only between
|
|
5
|
+
* `follow(true)` and `follow(false)`, which the controller says while the
|
|
6
|
+
* payer is on the waiting screen. Once `paymentId` is cleared (a sheet
|
|
7
|
+
* closing) the last answer stays.
|
|
8
|
+
*/
|
|
9
|
+
export function useCheckoutPayment(connection, paymentId) {
|
|
10
|
+
const [held, setHeld] = useState({
|
|
11
|
+
view: undefined,
|
|
12
|
+
failed: false,
|
|
13
|
+
});
|
|
14
|
+
const [following, setFollowing] = useState(false);
|
|
15
|
+
/** Opened again after a failure: once, then the calls read over HTTP. */
|
|
16
|
+
const [reopened, setReopened] = useState(0);
|
|
17
|
+
const failures = useRef(0);
|
|
18
|
+
/** Followed live: the connection brings each change, nothing is read again. */
|
|
19
|
+
const followingNow = useRef(false);
|
|
20
|
+
/** The payment held, and the newest answer shown: an older one never replaces it. */
|
|
21
|
+
const holding = useRef(paymentId);
|
|
22
|
+
const counter = useRef(0);
|
|
23
|
+
const shown = useRef(0);
|
|
24
|
+
const show = useCallback((ticket, next) => {
|
|
25
|
+
if (ticket < shown.current)
|
|
26
|
+
return;
|
|
27
|
+
shown.current = ticket;
|
|
28
|
+
setHeld(next);
|
|
29
|
+
}, []);
|
|
30
|
+
const read = useCallback((id) => {
|
|
31
|
+
const ticket = ++counter.current;
|
|
32
|
+
return connection.load(id).then((view) => {
|
|
33
|
+
if (holding.current === id)
|
|
34
|
+
show(ticket, () => ({ view, failed: false }));
|
|
35
|
+
},
|
|
36
|
+
// A failed read keeps what was shown.
|
|
37
|
+
() => {
|
|
38
|
+
if (holding.current === id)
|
|
39
|
+
show(ticket, (previous) => ({ ...previous, failed: true }));
|
|
40
|
+
});
|
|
41
|
+
}, [connection, show]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
holding.current = paymentId;
|
|
44
|
+
if (paymentId)
|
|
45
|
+
void read(paymentId);
|
|
46
|
+
}, [paymentId, read]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
followingNow.current = following;
|
|
49
|
+
}, [following]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!following || !paymentId)
|
|
52
|
+
return undefined;
|
|
53
|
+
return connection.follow(paymentId, (view) => {
|
|
54
|
+
failures.current = 0;
|
|
55
|
+
show(++counter.current, () => ({ view, failed: false }));
|
|
56
|
+
},
|
|
57
|
+
// A broken connection is read over HTTP and opened once more; broken
|
|
58
|
+
// again, it stays closed and the calls read over HTTP.
|
|
59
|
+
() => {
|
|
60
|
+
setHeld((previous) => ({ ...previous, failed: true }));
|
|
61
|
+
failures.current += 1;
|
|
62
|
+
if (failures.current > 1) {
|
|
63
|
+
setFollowing(false);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
void read(paymentId);
|
|
67
|
+
setReopened((count) => count + 1);
|
|
68
|
+
});
|
|
69
|
+
}, [following, reopened, paymentId, connection, show, read]);
|
|
70
|
+
const mutations = useMemo(() => {
|
|
71
|
+
const { open, ...changing } = connection.mutations;
|
|
72
|
+
/**
|
|
73
|
+
* The call, then the payment read again, then the call's answer. Not
|
|
74
|
+
* read while followed live, nor after a refusal: nothing changed.
|
|
75
|
+
*/
|
|
76
|
+
const reading = (call) => async (args) => {
|
|
77
|
+
const result = await call(args);
|
|
78
|
+
const refused = result?.ok === false;
|
|
79
|
+
if (!followingNow.current && !refused)
|
|
80
|
+
await read(args.paymentId);
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
open,
|
|
85
|
+
pay: reading(changing.pay),
|
|
86
|
+
payWithCode: reading(changing.payWithCode),
|
|
87
|
+
checkNow: reading(changing.checkNow),
|
|
88
|
+
cancel: reading(changing.cancel),
|
|
89
|
+
resend: reading(changing.resend),
|
|
90
|
+
simulate: reading(changing.simulate),
|
|
91
|
+
};
|
|
92
|
+
}, [connection, read]);
|
|
93
|
+
const refresh = useCallback(() => {
|
|
94
|
+
if (paymentId)
|
|
95
|
+
void read(paymentId);
|
|
96
|
+
}, [paymentId, read]);
|
|
97
|
+
const calls = useMemo(() => ({ mutations, follow: setFollowing, refresh }), [mutations, refresh]);
|
|
98
|
+
return { ...held, calls };
|
|
99
|
+
}
|
|
@@ -308,6 +308,10 @@ export interface CheckoutCore {
|
|
|
308
308
|
export interface CheckoutPlatform {
|
|
309
309
|
/** Calls `listener` each time the payer comes back to the page or the app; answers how to stop. */
|
|
310
310
|
onReturn: (listener: () => void) => () => void;
|
|
311
|
+
/** Calls `listener` each time the payer leaves the page or the app; answers how to stop. */
|
|
312
|
+
onLeave: (listener: () => void) => () => void;
|
|
313
|
+
/** The payer is away from the page or the app right now. */
|
|
314
|
+
isAway: () => boolean;
|
|
311
315
|
copyText: (text: string) => void;
|
|
312
316
|
/** Leaves for the app's return URL. */
|
|
313
317
|
openUrl: (url: string) => void;
|
|
@@ -323,9 +327,47 @@ export interface CheckoutPlatform {
|
|
|
323
327
|
export interface CheckoutControllerOptions {
|
|
324
328
|
clues: CountryClues;
|
|
325
329
|
/** Null on the Branding tab's preview: nothing is paid there. */
|
|
326
|
-
|
|
330
|
+
calls: CheckoutCalls | null;
|
|
327
331
|
platform: CheckoutPlatform;
|
|
328
332
|
}
|
|
333
|
+
/** What a checkout asks of the gateway about its payment, from `useCheckoutPayment`. */
|
|
334
|
+
export interface CheckoutCalls {
|
|
335
|
+
/** Each call that changes the payment reads it again before it answers. */
|
|
336
|
+
mutations: CheckoutMutations;
|
|
337
|
+
/** Opens or closes the live connection: open only while the payer is on the waiting screen. */
|
|
338
|
+
follow: (on: boolean) => void;
|
|
339
|
+
/** Reads the payment again: the card fields page started an attempt the checkout did not. */
|
|
340
|
+
refresh: () => void;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* How a checkout reaches the gateway (ADR 0006): plain calls, and a live
|
|
344
|
+
* connection to one payment only while the payer waits on it.
|
|
345
|
+
*/
|
|
346
|
+
export interface CheckoutConnection {
|
|
347
|
+
/** The payment's checkout view, read once; null: no payment has this id. */
|
|
348
|
+
load: (paymentId: string) => Promise<CheckoutView | null>;
|
|
349
|
+
/**
|
|
350
|
+
* Opens a live connection to one payment: `onView` with each answer,
|
|
351
|
+
* `onError` when it cannot load. Answers how to close it.
|
|
352
|
+
*/
|
|
353
|
+
follow: (paymentId: string, onView: (view: CheckoutView | null) => void, onError: () => void) => () => void;
|
|
354
|
+
mutations: CheckoutMutations;
|
|
355
|
+
}
|
|
356
|
+
/** What a checkout last heard of its payment. */
|
|
357
|
+
export interface HeldPayment {
|
|
358
|
+
/** `undefined` while loading, null for a link that matches none. */
|
|
359
|
+
view: CheckoutView | null | undefined;
|
|
360
|
+
/** The last read failed. */
|
|
361
|
+
failed: boolean;
|
|
362
|
+
}
|
|
363
|
+
/** One payment as a checkout holds it, from `useCheckoutPayment`. */
|
|
364
|
+
export interface CheckoutPayment extends HeldPayment {
|
|
365
|
+
calls: CheckoutCalls;
|
|
366
|
+
}
|
|
367
|
+
/** What a gateway call answers when it can refuse (Pay, Send again). */
|
|
368
|
+
export interface MaybeRefused {
|
|
369
|
+
ok?: boolean;
|
|
370
|
+
}
|
|
329
371
|
/** What Pay answers: refused with a reason, or an attempt started. */
|
|
330
372
|
export interface PayResult {
|
|
331
373
|
ok: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BrissotoPayProviderProps } from '../models/sheet.interface.js';
|
|
2
2
|
/**
|
|
3
|
-
* Around the app:
|
|
4
|
-
*
|
|
3
|
+
* Around the app: the calls to Brissoto Pay and the one payment sheet
|
|
4
|
+
* `usePaymentSheet().presentPayment` opens. Nothing stays connected: a
|
|
5
|
+
* sheet follows its payment live only while the payer waits on it.
|
|
5
6
|
*/
|
|
6
7
|
export declare function BrissotoPayProvider({ url, children, }: BrissotoPayProviderProps): import("react").JSX.Element;
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
3
|
import { PaymentSheetContext } from "../hooks/use-payment-sheet.js";
|
|
5
4
|
import { DEFAULT_URL } from "../models/sheet.constants.js";
|
|
6
|
-
import {
|
|
5
|
+
import { convexCheckoutConnection } from "../services/checkout.service.js";
|
|
7
6
|
import { PaymentSheetModal } from "./PaymentSheetModal.js";
|
|
8
7
|
/**
|
|
9
|
-
* Around the app:
|
|
10
|
-
*
|
|
8
|
+
* Around the app: the calls to Brissoto Pay and the one payment sheet
|
|
9
|
+
* `usePaymentSheet().presentPayment` opens. Nothing stays connected: a
|
|
10
|
+
* sheet follows its payment live only while the payer waits on it.
|
|
11
11
|
*/
|
|
12
12
|
export function BrissotoPayProvider({ url = DEFAULT_URL, children, }) {
|
|
13
|
-
const
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
const next = new ConvexReactClient(url, { unsavedChangesWarning: false });
|
|
16
|
-
setClient(next);
|
|
17
|
-
return () => void next.close();
|
|
18
|
-
}, [url]);
|
|
19
|
-
const calls = useMemo(() => client && convexCheckoutCalls(client), [client]);
|
|
13
|
+
const connection = useMemo(() => convexCheckoutConnection(url), [url]);
|
|
20
14
|
const [request, setRequest] = useState(null);
|
|
21
15
|
/** Sheets opened, so each gets a fresh one. */
|
|
22
16
|
const [count, setCount] = useState(0);
|
|
@@ -37,5 +31,5 @@ export function BrissotoPayProvider({ url = DEFAULT_URL, children, }) {
|
|
|
37
31
|
setRequest({ ...closing, open: false });
|
|
38
32
|
closing.resolve(result);
|
|
39
33
|
};
|
|
40
|
-
return (_jsxs(PaymentSheetContext, { value: sheet, children: [children,
|
|
34
|
+
return (_jsxs(PaymentSheetContext, { value: sheet, children: [children, request && (_jsx(PaymentSheetModal, { connection: connection, request: request, onClose: (result) => close(request, result) }, count))] }));
|
|
41
35
|
}
|
|
@@ -5,4 +5,4 @@ import type { PaymentSheetModalProps } from '../models/sheet.interface.js';
|
|
|
5
5
|
* a swipe down, a tap on the app behind, back or escape leave it; leaving
|
|
6
6
|
* while a prompt waits on the phone asks first, and resolves `pending`.
|
|
7
7
|
*/
|
|
8
|
-
export declare function PaymentSheetModal({
|
|
8
|
+
export declare function PaymentSheetModal({ connection, request, onClose, }: PaymentSheetModalProps): import("react").JSX.Element;
|
|
@@ -4,9 +4,9 @@ import { useMemo, useState } from 'react';
|
|
|
4
4
|
import { Alert, Animated, KeyboardAvoidingView, Modal, Pressable, StyleSheet, View, useColorScheme, } from 'react-native';
|
|
5
5
|
import { IntlProvider, createTranslator } from 'use-intl';
|
|
6
6
|
import { CHECKOUT_COLORS, INVALID_BRAND_DARK, INVALID_BRAND_LIGHT, } from "../checkout-core/models/checkout.constants.js";
|
|
7
|
+
import { useCheckoutPayment } from "../checkout-core/hooks/use-checkout-payment.js";
|
|
7
8
|
import { CheckoutThemeEnum } from "../checkout-core/models/checkout.enum.js";
|
|
8
9
|
import { useSheetMotion } from "../hooks/use-sheet-motion.js";
|
|
9
|
-
import { useWatchedPayment } from "../hooks/use-watched-payment.js";
|
|
10
10
|
import { SheetThemeContext } from "../hooks/use-sheet-theme.js";
|
|
11
11
|
import { FIGTREE_FILES, MESSAGES, TIME_ZONE, } from "../models/sheet.constants.js";
|
|
12
12
|
import { sheetLanguage, sheetResult, sheetTheme, } from "../models/sheet.utils.js";
|
|
@@ -17,9 +17,9 @@ import { SheetContent } from "./SheetContent.js";
|
|
|
17
17
|
* a swipe down, a tap on the app behind, back or escape leave it; leaving
|
|
18
18
|
* while a prompt waits on the phone asks first, and resolves `pending`.
|
|
19
19
|
*/
|
|
20
|
-
export function PaymentSheetModal({
|
|
21
|
-
const
|
|
22
|
-
const { view } =
|
|
20
|
+
export function PaymentSheetModal({ connection, request, onClose, }) {
|
|
21
|
+
const payment = useCheckoutPayment(connection, request.open ? request.paymentId : null);
|
|
22
|
+
const { view } = payment;
|
|
23
23
|
const scheme = useColorScheme();
|
|
24
24
|
const [deviceLocale] = useState(() => Intl.DateTimeFormat().resolvedOptions().locale);
|
|
25
25
|
const [fontsLoaded, fontsError] = useFonts(FIGTREE_FILES);
|
|
@@ -32,7 +32,7 @@ export function PaymentSheetModal({ calls, request, onClose, }) {
|
|
|
32
32
|
dark,
|
|
33
33
|
}), [dark, color]);
|
|
34
34
|
const close = () => {
|
|
35
|
-
const result = sheetResult(view,
|
|
35
|
+
const result = sheetResult(view, payment.failed);
|
|
36
36
|
if (result.status !== 'pending') {
|
|
37
37
|
onClose(result);
|
|
38
38
|
return;
|
|
@@ -63,7 +63,7 @@ export function PaymentSheetModal({ calls, request, onClose, }) {
|
|
|
63
63
|
], children: _jsx(View, { style: [styles.stage, { paddingTop: motion.topGap }], children: _jsxs(KeyboardAvoidingView, { behavior: "padding", style: styles.sheet, children: [_jsx(View, { testID: "payment-sheet-floor", style: [
|
|
64
64
|
styles.floor,
|
|
65
65
|
{ backgroundColor: theme.colors.surface },
|
|
66
|
-
] }), _jsx(IntlProvider, { locale: lang, messages: MESSAGES[lang], timeZone: TIME_ZONE, children: _jsx(SheetThemeContext, { value: theme, children: _jsx(SheetContent, {
|
|
66
|
+
] }), _jsx(IntlProvider, { locale: lang, messages: MESSAGES[lang], timeZone: TIME_ZONE, children: _jsx(SheetThemeContext, { value: theme, children: _jsx(SheetContent, { payment: payment, fontsReady: fontsLoaded || fontsError !== null, deviceLocale: deviceLocale, onClose: close, dragHandlers: motion.dragHandlers }) }) })] }) }) })] }) }));
|
|
67
67
|
}
|
|
68
68
|
const styles = StyleSheet.create({
|
|
69
69
|
screen: { flex: 1 },
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { SheetContentProps } from '../models/sheet.interface.js';
|
|
2
2
|
/** The sheet's screens, driven by the shared checkout controller. */
|
|
3
|
-
export declare function SheetContent({
|
|
3
|
+
export declare function SheetContent({ payment, fontsReady, deviceLocale, onClose, dragHandlers, }: SheetContentProps): import("react").JSX.Element;
|
|
@@ -28,7 +28,7 @@ const TERMINAL_KIND = {
|
|
|
28
28
|
[CheckoutScreenEnum.NO_METHOD]: TerminalKindEnum.NO_METHOD,
|
|
29
29
|
};
|
|
30
30
|
/** The sheet's screens, driven by the shared checkout controller. */
|
|
31
|
-
export function SheetContent({
|
|
31
|
+
export function SheetContent({ payment, fontsReady, deviceLocale, onClose, dragHandlers, }) {
|
|
32
32
|
const t = useTranslations('checkout');
|
|
33
33
|
const { colors } = useSheetTheme();
|
|
34
34
|
const [clues] = useState(() => ({
|
|
@@ -37,16 +37,16 @@ export function SheetContent({ calls, watched, fontsReady, deviceLocale, onClose
|
|
|
37
37
|
}));
|
|
38
38
|
const phoneRef = useRef(null);
|
|
39
39
|
const { platform, cardFields } = useNativePlatform(onClose, phoneRef);
|
|
40
|
-
const { view } =
|
|
40
|
+
const { view } = payment;
|
|
41
41
|
const core = useCheckoutController(view, {
|
|
42
42
|
clues,
|
|
43
|
-
|
|
43
|
+
calls: payment.calls,
|
|
44
44
|
platform,
|
|
45
45
|
});
|
|
46
46
|
const { ui, derived, actions } = core;
|
|
47
47
|
const { screen } = derived;
|
|
48
48
|
// A payment that could not load is not loading any more.
|
|
49
|
-
const failed =
|
|
49
|
+
const failed = payment.failed && view === undefined;
|
|
50
50
|
const loading = !fontsReady || (screen === CheckoutScreenEnum.LOADING && !failed);
|
|
51
51
|
const terminal = TERMINAL_KIND[screen];
|
|
52
52
|
useSheetResize([
|
|
@@ -22,6 +22,15 @@ export function useNativePlatform(onLeave, phoneField) {
|
|
|
22
22
|
});
|
|
23
23
|
return () => subscription.remove();
|
|
24
24
|
},
|
|
25
|
+
// Inactive (a notification pulled down) is not leaving.
|
|
26
|
+
onLeave: (listener) => {
|
|
27
|
+
const subscription = AppState.addEventListener('change', (state) => {
|
|
28
|
+
if (state === 'background')
|
|
29
|
+
listener();
|
|
30
|
+
});
|
|
31
|
+
return () => subscription.remove();
|
|
32
|
+
},
|
|
33
|
+
isAway: () => AppState.currentState === 'background',
|
|
25
34
|
// No clipboard in React Native itself: the share sheet offers Copy.
|
|
26
35
|
copyText: (text) => {
|
|
27
36
|
void Share.share({ message: text }).catch(() => undefined);
|
|
@@ -3,7 +3,7 @@ import type { Animated, DimensionValue, GestureResponderHandlers, StyleProp, Tex
|
|
|
3
3
|
import type { WebView } from 'react-native-webview';
|
|
4
4
|
import type { CheckoutButtonVariantEnum, TerminalKindEnum } from '../checkout-core/models/checkout.enum.js';
|
|
5
5
|
import type { LangEnum, ProviderLogoEnum } from '../checkout-core/models/domain.enum.js';
|
|
6
|
-
import type { CardFieldsMessage, CheckoutColors, CheckoutCore, CheckoutToast,
|
|
6
|
+
import type { CardFieldsMessage, CheckoutColors, CheckoutCore, CheckoutToast, CheckoutConnection, CheckoutPayment, CheckoutPlatform, CheckoutView } from '../checkout-core/models/checkout.interface.js';
|
|
7
7
|
import type { FigtreeEnum, SheetIconEnum } from './sheet.enum.js';
|
|
8
8
|
/**
|
|
9
9
|
* How the sheet ended. `closed`: the payer closed it with the payment still
|
|
@@ -29,15 +29,6 @@ export interface BrissotoPayProviderProps {
|
|
|
29
29
|
url?: string;
|
|
30
30
|
children?: ReactNode;
|
|
31
31
|
}
|
|
32
|
-
/** The checkout's gateway calls, one interface for the sheet whatever client is behind it. */
|
|
33
|
-
export interface CheckoutCalls {
|
|
34
|
-
/**
|
|
35
|
-
* Follows one payment's checkout view live: `onView` with each answer
|
|
36
|
-
* (null: no payment has this id), `onError` when it cannot load. Answers how to stop.
|
|
37
|
-
*/
|
|
38
|
-
watch: (paymentId: string, onView: (view: CheckoutView | null) => void, onError: () => void) => () => void;
|
|
39
|
-
mutations: CheckoutMutations;
|
|
40
|
-
}
|
|
41
32
|
/** The sheet the app asked for, and how to answer it. */
|
|
42
33
|
export interface SheetRequest {
|
|
43
34
|
paymentId: string;
|
|
@@ -45,20 +36,13 @@ export interface SheetRequest {
|
|
|
45
36
|
open: boolean;
|
|
46
37
|
resolve: (result: PaymentSheetResult) => void;
|
|
47
38
|
}
|
|
48
|
-
/** A payment as the sheet follows it: undefined while loading, null for an id that matches none. */
|
|
49
|
-
export interface WatchedPayment {
|
|
50
|
-
view: CheckoutView | null | undefined;
|
|
51
|
-
/** It could not load. */
|
|
52
|
-
failed: boolean;
|
|
53
|
-
}
|
|
54
39
|
export interface PaymentSheetModalProps {
|
|
55
|
-
|
|
40
|
+
connection: CheckoutConnection;
|
|
56
41
|
request: SheetRequest;
|
|
57
42
|
onClose: (result: PaymentSheetResult) => void;
|
|
58
43
|
}
|
|
59
44
|
export interface SheetContentProps {
|
|
60
|
-
|
|
61
|
-
watched: WatchedPayment;
|
|
45
|
+
payment: CheckoutPayment;
|
|
62
46
|
/** Fonts still loading: the sheet shows its skeleton. */
|
|
63
47
|
fontsReady: boolean;
|
|
64
48
|
deviceLocale: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { CheckoutCalls } from '../models/sheet.interface.js';
|
|
1
|
+
import type { CheckoutConnection } from '../checkout-core/models/checkout.interface.js';
|
|
3
2
|
/**
|
|
4
|
-
* The checkout's calls
|
|
5
|
-
*
|
|
3
|
+
* The checkout's calls to the gateway at `url` (ADR 0006): plain HTTP calls,
|
|
4
|
+
* by path, to the same public functions the web checkout calls, so the
|
|
5
|
+
* package needs none of the backend's code. A live connection is opened only
|
|
6
|
+
* to follow one payment, and closed with it.
|
|
6
7
|
*/
|
|
7
|
-
export declare function
|
|
8
|
+
export declare function convexCheckoutConnection(url: string): CheckoutConnection;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { ConvexClient, ConvexHttpClient } from 'convex/browser';
|
|
1
2
|
import { makeFunctionReference } from 'convex/server';
|
|
2
3
|
import { CHECKOUT_FUNCTIONS } from "../models/sheet.constants.js";
|
|
3
4
|
/**
|
|
4
|
-
* The checkout's calls
|
|
5
|
-
*
|
|
5
|
+
* The checkout's calls to the gateway at `url` (ADR 0006): plain HTTP calls,
|
|
6
|
+
* by path, to the same public functions the web checkout calls, so the
|
|
7
|
+
* package needs none of the backend's code. A live connection is opened only
|
|
8
|
+
* to follow one payment, and closed with it.
|
|
6
9
|
*/
|
|
7
|
-
export function
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
+
export function convexCheckoutConnection(url) {
|
|
11
|
+
const http = new ConvexHttpClient(url);
|
|
12
|
+
const mutation = (path) => (args) => http.mutation(makeFunctionReference(path), args);
|
|
13
|
+
const action = (path) => (args) => http.action(makeFunctionReference(path), args);
|
|
14
|
+
const get = makeFunctionReference(CHECKOUT_FUNCTIONS.get);
|
|
10
15
|
const mutations = {
|
|
11
16
|
pay: mutation(CHECKOUT_FUNCTIONS.pay),
|
|
12
17
|
payWithCode: action(CHECKOUT_FUNCTIONS.payWithCode),
|
|
@@ -17,22 +22,14 @@ export function convexCheckoutCalls(client) {
|
|
|
17
22
|
simulate: mutation(CHECKOUT_FUNCTIONS.simulate),
|
|
18
23
|
};
|
|
19
24
|
return {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
catch {
|
|
28
|
-
onError();
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (view !== undefined)
|
|
32
|
-
onView(view);
|
|
25
|
+
load: (paymentId) => http.query(get, { paymentId }),
|
|
26
|
+
follow: (paymentId, onView, onError) => {
|
|
27
|
+
const socket = new ConvexClient(url, { unsavedChangesWarning: false });
|
|
28
|
+
const stop = socket.onUpdate(get, { paymentId }, (view) => onView(view), () => onError());
|
|
29
|
+
return () => {
|
|
30
|
+
stop();
|
|
31
|
+
void socket.close();
|
|
33
32
|
};
|
|
34
|
-
read();
|
|
35
|
-
return watch.onUpdate(read);
|
|
36
33
|
},
|
|
37
34
|
mutations,
|
|
38
35
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { CheckoutCalls, WatchedPayment } from '../models/sheet.interface.js';
|
|
2
|
-
/**
|
|
3
|
-
* One payment, live, while `paymentId` is set. Once it is cleared (the sheet
|
|
4
|
-
* closing) the last answer stays, so the sheet slides away unchanged.
|
|
5
|
-
*/
|
|
6
|
-
export declare function useWatchedPayment(calls: CheckoutCalls, paymentId: string | null): WatchedPayment;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
/**
|
|
3
|
-
* One payment, live, while `paymentId` is set. Once it is cleared (the sheet
|
|
4
|
-
* closing) the last answer stays, so the sheet slides away unchanged.
|
|
5
|
-
*/
|
|
6
|
-
export function useWatchedPayment(calls, paymentId) {
|
|
7
|
-
const [watched, setWatched] = useState({
|
|
8
|
-
view: undefined,
|
|
9
|
-
failed: false,
|
|
10
|
-
});
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (!paymentId)
|
|
13
|
-
return undefined;
|
|
14
|
-
return calls.watch(paymentId, (view) => setWatched({ view, failed: false }), () => setWatched((previous) => ({ ...previous, failed: true })));
|
|
15
|
-
}, [calls, paymentId]);
|
|
16
|
-
return watched;
|
|
17
|
-
}
|