@heyama/pay-react-native 1.0.1 → 1.0.3
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.js +25 -21
- package/dist/checkout-core/hooks/use-checkout-timers.d.ts +6 -3
- package/dist/checkout-core/hooks/use-checkout-timers.js +6 -6
- package/dist/checkout-core/messages/en.d.ts +1 -0
- package/dist/checkout-core/messages/en.js +1 -0
- package/dist/checkout-core/messages/fr.js +1 -0
- package/dist/checkout-core/models/checkout.enum.d.ts +8 -0
- package/dist/checkout-core/models/checkout.enum.js +9 -0
- package/dist/checkout-core/models/checkout.interface.d.ts +14 -2
- package/dist/checkout-core/models/checkout.view.utils.js +1 -0
- package/dist/components/SheetButton.d.ts +1 -1
- package/dist/components/SheetButton.js +8 -4
- package/dist/components/SheetChosenMethod.js +3 -2
- package/dist/components/SheetCodeField.js +4 -3
- package/dist/components/SheetContent.js +1 -1
- package/dist/components/SheetFailed.js +3 -3
- package/dist/components/SheetForm.js +17 -8
- package/dist/components/SheetHeader.js +9 -8
- package/dist/components/SheetIcon.js +11 -3
- package/dist/components/SheetLayout.js +5 -3
- package/dist/components/SheetMessage.js +1 -1
- package/dist/components/SheetMethodCard.d.ts +1 -1
- package/dist/components/SheetMethodCard.js +19 -4
- package/dist/components/SheetPhoneField.js +4 -3
- package/dist/components/SheetSimulator.js +1 -1
- package/dist/components/SheetSuccess.js +1 -1
- package/dist/components/SheetText.d.ts +2 -2
- package/dist/components/SheetText.js +4 -2
- package/dist/components/SheetToast.d.ts +6 -2
- package/dist/components/SheetToast.js +58 -11
- package/dist/components/SheetWaiting.js +1 -1
- package/dist/hooks/use-shake.d.ts +3 -0
- package/dist/hooks/use-shake.js +18 -0
- package/dist/models/sheet.constants.d.ts +23 -0
- package/dist/models/sheet.constants.js +23 -0
- package/dist/models/sheet.interface.d.ts +8 -2
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useFormatter, useTranslations } from 'use-intl';
|
|
3
3
|
import { COPIED_MS, FLOW_TRAITS, OTP_MAX_LENGTH, OTP_PATTERN, } from "../models/checkout.constants.js";
|
|
4
|
-
import { CardFieldsMessageEnum, CheckoutScreenEnum, CheckoutTabEnum, MethodFlowEnum, MethodStateEnum, PayRefusalEnum, } from "../models/checkout.enum.js";
|
|
4
|
+
import { CardFieldsMessageEnum, CheckoutScreenEnum, CheckoutTabEnum, CheckoutToastToneEnum, MethodFlowEnum, MethodStateEnum, PayRefusalEnum, } from "../models/checkout.enum.js";
|
|
5
5
|
import { CountryEnum } from "../models/domain.enum.js";
|
|
6
6
|
import { cardLabel } from "../models/checkout.card.utils.js";
|
|
7
7
|
import { formatMoney } from "../models/checkout.money.utils.js";
|
|
8
8
|
import { maskE164 } from "../models/checkout.phone.utils.js";
|
|
9
|
-
import { countriesOf, deriveCheckout, initialUi, screenOf, waitLeftOf, } from "../models/checkout.view.utils.js";
|
|
9
|
+
import { countriesOf, deriveCheckout, initialUi, logoOf, screenOf, waitLeftOf, } from "../models/checkout.view.utils.js";
|
|
10
10
|
import { useNow, useReturnCountdown, useToast } from "./use-checkout-timers.js";
|
|
11
11
|
import { usePhoneNumber } from "./use-phone-number.js";
|
|
12
12
|
/** Before the payment has loaded, or for a link that matches none. */
|
|
@@ -22,6 +22,7 @@ const NO_PAYMENT_UI = {
|
|
|
22
22
|
submitting: false,
|
|
23
23
|
dismissed: null,
|
|
24
24
|
redirecting: false,
|
|
25
|
+
methodMissing: 0,
|
|
25
26
|
};
|
|
26
27
|
/**
|
|
27
28
|
* The whole checkout for one payment, on any platform: the gateway's live
|
|
@@ -84,7 +85,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
84
85
|
if (!waiting || waitLeft > 0)
|
|
85
86
|
return;
|
|
86
87
|
update({ dismissed: attemptReference });
|
|
87
|
-
showToast(noAnswer);
|
|
88
|
+
showToast(noAnswer, CheckoutToastToneEnum.WARNING);
|
|
88
89
|
}, [waiting, waitLeft, attemptReference, noAnswer, showToast, update]);
|
|
89
90
|
const paymentId = view?.id;
|
|
90
91
|
// A payer opened the link, said once per page (the dashboard's preview has no mutations).
|
|
@@ -131,7 +132,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
131
132
|
break;
|
|
132
133
|
case CardFieldsMessageEnum.REFUSED:
|
|
133
134
|
update({ cardPaying: false });
|
|
134
|
-
showToast(message.reason ? refusals(message.reason) : failedToast);
|
|
135
|
+
showToast(message.reason ? refusals(message.reason) : failedToast, CheckoutToastToneEnum.WARNING);
|
|
135
136
|
break;
|
|
136
137
|
}
|
|
137
138
|
};
|
|
@@ -139,7 +140,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
139
140
|
}, [update, showToast, refusals, failedToast, platform]);
|
|
140
141
|
/* ---------------------------------------------------------- helpers */
|
|
141
142
|
const reference = attempt?.reference ?? view?.id ?? '';
|
|
142
|
-
const comingSoon = (name) => toast.show(t('toast.soon', { method: name }));
|
|
143
|
+
const comingSoon = (name) => toast.show(t('toast.soon', { method: name }), CheckoutToastToneEnum.INFO);
|
|
143
144
|
/**
|
|
144
145
|
* Starts an attempt with the number, and the payment code of an OTP
|
|
145
146
|
* method; a second tap while one is on its way does nothing.
|
|
@@ -155,9 +156,9 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
155
156
|
: mutations.payWithCode({ ...args, otp }))
|
|
156
157
|
.then((result) => {
|
|
157
158
|
if (!result.ok)
|
|
158
|
-
toast.show(t(`refusals.${result.reason}`));
|
|
159
|
+
toast.show(t(`refusals.${result.reason}`), CheckoutToastToneEnum.WARNING);
|
|
159
160
|
})
|
|
160
|
-
.catch(() => toast.show(t('toast.failed')))
|
|
161
|
+
.catch(() => toast.show(t('toast.failed'), CheckoutToastToneEnum.WARNING))
|
|
161
162
|
.finally(() => {
|
|
162
163
|
paying.current = false;
|
|
163
164
|
update({ submitting: false });
|
|
@@ -188,7 +189,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
188
189
|
comingSoon(method.name);
|
|
189
190
|
}
|
|
190
191
|
else if (method.state === MethodStateEnum.MAINTENANCE) {
|
|
191
|
-
toast.show(method.note ?? t('toast.unavailable', { method: method.name }));
|
|
192
|
+
toast.show(method.note ?? t('toast.unavailable', { method: method.name }), CheckoutToastToneEnum.WARNING, logoOf(method));
|
|
192
193
|
}
|
|
193
194
|
else {
|
|
194
195
|
update({
|
|
@@ -196,6 +197,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
196
197
|
pickerOpen: false,
|
|
197
198
|
code: '',
|
|
198
199
|
cardComplete: false,
|
|
200
|
+
methodMissing: 0,
|
|
199
201
|
});
|
|
200
202
|
}
|
|
201
203
|
},
|
|
@@ -205,12 +207,14 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
205
207
|
switchToDetected: () => {
|
|
206
208
|
const detected = derived.mismatch;
|
|
207
209
|
update({ method: detected.id });
|
|
208
|
-
toast.show(t('toast.switched', { method: detected.name }));
|
|
210
|
+
toast.show(t('toast.switched', { method: detected.name }), CheckoutToastToneEnum.SUCCESS, logoOf(detected));
|
|
209
211
|
},
|
|
210
212
|
pay: () => {
|
|
211
213
|
const { selected } = derived;
|
|
212
|
-
if (!selected)
|
|
214
|
+
if (!selected) {
|
|
215
|
+
update({ methodMissing: ui.methodMissing + 1 });
|
|
213
216
|
return;
|
|
217
|
+
}
|
|
214
218
|
// The preview offers no card: it shows Mobile Money only.
|
|
215
219
|
if (selected.flow === MethodFlowEnum.CARD_FIELDS) {
|
|
216
220
|
// The fields page pays with its provider, then says how it went.
|
|
@@ -225,11 +229,11 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
225
229
|
}
|
|
226
230
|
const otp = selected.flow === MethodFlowEnum.OTP;
|
|
227
231
|
if (otp && !OTP_PATTERN.test(ui.code)) {
|
|
228
|
-
toast.show(t(`refusals.${PayRefusalEnum.CODE}`));
|
|
232
|
+
toast.show(t(`refusals.${PayRefusalEnum.CODE}`), CheckoutToastToneEnum.WARNING);
|
|
229
233
|
return;
|
|
230
234
|
}
|
|
231
235
|
if (!mutations) {
|
|
232
|
-
toast.show(t('toast.preview'));
|
|
236
|
+
toast.show(t('toast.preview'), CheckoutToastToneEnum.INFO);
|
|
233
237
|
return;
|
|
234
238
|
}
|
|
235
239
|
submit(selected.id, {
|
|
@@ -240,7 +244,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
240
244
|
// Only offered while an OTP method, which always has its code, is picked.
|
|
241
245
|
cannotDial: () => {
|
|
242
246
|
const method = derived.selected;
|
|
243
|
-
toast.show(t('toast.dial', { code: method.otpCode, method: method.name }));
|
|
247
|
+
toast.show(t('toast.dial', { code: method.otpCode, method: method.name }), CheckoutToastToneEnum.INFO, logoOf(method));
|
|
244
248
|
},
|
|
245
249
|
// Only on the failure screen, which the preview never shows.
|
|
246
250
|
retry: () => {
|
|
@@ -273,15 +277,15 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
273
277
|
resend: () => {
|
|
274
278
|
void mutations
|
|
275
279
|
.resend({ paymentId: view.id })
|
|
276
|
-
.then((result) =>
|
|
277
|
-
? t('toast.resent', { phone: maskE164(attempt.phone) })
|
|
278
|
-
: t('toast.resend_wait')))
|
|
279
|
-
.catch(() => toast.show(t('toast.failed')));
|
|
280
|
+
.then((result) => result.ok
|
|
281
|
+
? toast.show(t('toast.resent', { phone: maskE164(attempt.phone) }), CheckoutToastToneEnum.SUCCESS)
|
|
282
|
+
: toast.show(t('toast.resend_wait'), CheckoutToastToneEnum.INFO))
|
|
283
|
+
.catch(() => toast.show(t('toast.failed'), CheckoutToastToneEnum.WARNING));
|
|
280
284
|
},
|
|
281
285
|
cancel: () => {
|
|
282
286
|
void mutations
|
|
283
287
|
.cancel({ paymentId: view.id })
|
|
284
|
-
.catch(() => toast.show(t('toast.failed')));
|
|
288
|
+
.catch(() => toast.show(t('toast.failed'), CheckoutToastToneEnum.WARNING));
|
|
285
289
|
},
|
|
286
290
|
returnNow: goBack,
|
|
287
291
|
stayHere: countdown.stop,
|
|
@@ -292,11 +296,11 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
292
296
|
clearTimeout(copiedTimer.current);
|
|
293
297
|
setCopied(true);
|
|
294
298
|
copiedTimer.current = setTimeout(() => setCopied(false), COPIED_MS);
|
|
295
|
-
toast.show(t('toast.copied'));
|
|
299
|
+
toast.show(t('toast.copied'), CheckoutToastToneEnum.SUCCESS);
|
|
296
300
|
},
|
|
297
301
|
downloadReceipt: () => {
|
|
298
302
|
platform.saveReceipt(`receipt-${reference}.txt`, receiptText(view));
|
|
299
|
-
toast.show(t('toast.downloaded'));
|
|
303
|
+
toast.show(t('toast.downloaded'), CheckoutToastToneEnum.SUCCESS);
|
|
300
304
|
},
|
|
301
305
|
simulate: (outcome) => {
|
|
302
306
|
void mutations.simulate({ paymentId: view.id, outcome });
|
|
@@ -310,7 +314,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
310
314
|
waitLeft,
|
|
311
315
|
returnLeft: countdown.left,
|
|
312
316
|
helpOpen,
|
|
313
|
-
|
|
317
|
+
toast: toast.toast,
|
|
314
318
|
copied,
|
|
315
319
|
preview: !mutations,
|
|
316
320
|
cardHeight,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { CheckoutToastToneEnum } from '../models/checkout.enum.js';
|
|
2
|
+
import type { ProviderLogoEnum } from '../models/domain.enum.js';
|
|
3
|
+
import type { CheckoutToast } from '../models/checkout.interface.js';
|
|
1
4
|
/** The time now, moving once a second while `running`: the waiting screen's clock. */
|
|
2
5
|
export declare function useNow(running: boolean): number;
|
|
3
6
|
/**
|
|
@@ -8,8 +11,8 @@ export declare function useReturnCountdown(running: boolean, onDone: () => void)
|
|
|
8
11
|
left: number | null;
|
|
9
12
|
stop: () => void;
|
|
10
13
|
};
|
|
11
|
-
/** One short message at a time at the top of the page. */
|
|
14
|
+
/** One short message at a time at the top of the page, with its tone and the method it is about. */
|
|
12
15
|
export declare function useToast(): {
|
|
13
|
-
|
|
14
|
-
show: (
|
|
16
|
+
toast: CheckoutToast | null;
|
|
17
|
+
show: (message: string, tone: CheckoutToastToneEnum, logo?: ProviderLogoEnum | null) => void;
|
|
15
18
|
};
|
|
@@ -30,15 +30,15 @@ export function useReturnCountdown(running, onDone) {
|
|
|
30
30
|
}, [running, left, onDone]);
|
|
31
31
|
return { left, stop: () => setLeft(null) };
|
|
32
32
|
}
|
|
33
|
-
/** One short message at a time at the top of the page. */
|
|
33
|
+
/** One short message at a time at the top of the page, with its tone and the method it is about. */
|
|
34
34
|
export function useToast() {
|
|
35
|
-
const [
|
|
35
|
+
const [toast, setToast] = useState(null);
|
|
36
36
|
const timer = useRef(undefined);
|
|
37
37
|
useEffect(() => () => clearTimeout(timer.current), []);
|
|
38
|
-
const show = useCallback((
|
|
38
|
+
const show = useCallback((message, tone, logo = null) => {
|
|
39
39
|
clearTimeout(timer.current);
|
|
40
|
-
|
|
41
|
-
timer.current = setTimeout(() =>
|
|
40
|
+
setToast({ message, tone, logo });
|
|
41
|
+
timer.current = setTimeout(() => setToast(null), TOAST_MS);
|
|
42
42
|
}, []);
|
|
43
|
-
return {
|
|
43
|
+
return { toast, show };
|
|
44
44
|
}
|
|
@@ -123,6 +123,8 @@ export declare enum TerminalKindEnum {
|
|
|
123
123
|
export declare enum CheckoutButtonVariantEnum {
|
|
124
124
|
PRIMARY = "primary",
|
|
125
125
|
SECONDARY = "secondary",
|
|
126
|
+
/** The second choice under Pay or Try again: the secondary's look, no border. */
|
|
127
|
+
PLAIN = "plain",
|
|
126
128
|
/** Gives something up: cancelling the payment. */
|
|
127
129
|
DANGER = "danger"
|
|
128
130
|
}
|
|
@@ -131,3 +133,9 @@ export declare enum CheckoutThemeEnum {
|
|
|
131
133
|
LIGHT = "light",
|
|
132
134
|
DARK = "dark"
|
|
133
135
|
}
|
|
136
|
+
/** What a toast says: done, a problem, or just good to know. Sets its icon. */
|
|
137
|
+
export declare enum CheckoutToastToneEnum {
|
|
138
|
+
SUCCESS = "success",
|
|
139
|
+
WARNING = "warning",
|
|
140
|
+
INFO = "info"
|
|
141
|
+
}
|
|
@@ -139,6 +139,8 @@ export var CheckoutButtonVariantEnum;
|
|
|
139
139
|
(function (CheckoutButtonVariantEnum) {
|
|
140
140
|
CheckoutButtonVariantEnum["PRIMARY"] = "primary";
|
|
141
141
|
CheckoutButtonVariantEnum["SECONDARY"] = "secondary";
|
|
142
|
+
/** The second choice under Pay or Try again: the secondary's look, no border. */
|
|
143
|
+
CheckoutButtonVariantEnum["PLAIN"] = "plain";
|
|
142
144
|
/** Gives something up: cancelling the payment. */
|
|
143
145
|
CheckoutButtonVariantEnum["DANGER"] = "danger";
|
|
144
146
|
})(CheckoutButtonVariantEnum || (CheckoutButtonVariantEnum = {}));
|
|
@@ -148,3 +150,10 @@ export var CheckoutThemeEnum;
|
|
|
148
150
|
CheckoutThemeEnum["LIGHT"] = "light";
|
|
149
151
|
CheckoutThemeEnum["DARK"] = "dark";
|
|
150
152
|
})(CheckoutThemeEnum || (CheckoutThemeEnum = {}));
|
|
153
|
+
/** What a toast says: done, a problem, or just good to know. Sets its icon. */
|
|
154
|
+
export var CheckoutToastToneEnum;
|
|
155
|
+
(function (CheckoutToastToneEnum) {
|
|
156
|
+
CheckoutToastToneEnum["SUCCESS"] = "success";
|
|
157
|
+
CheckoutToastToneEnum["WARNING"] = "warning";
|
|
158
|
+
CheckoutToastToneEnum["INFO"] = "info";
|
|
159
|
+
})(CheckoutToastToneEnum || (CheckoutToastToneEnum = {}));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CardFieldsMessageEnum, CheckoutPaymentStatusEnum, CheckoutScreenEnum, CheckoutTabEnum, CheckoutThemeEnum, MethodBrandEnum, MethodFlowEnum, MethodKindEnum, MethodStateEnum, PayRefusalEnum, PhoneErrorEnum, SimulatorOutcomeEnum, SupportChannelEnum } from './checkout.enum.js';
|
|
1
|
+
import type { CardFieldsMessageEnum, CheckoutPaymentStatusEnum, CheckoutToastToneEnum, CheckoutScreenEnum, CheckoutTabEnum, CheckoutThemeEnum, MethodBrandEnum, MethodFlowEnum, MethodKindEnum, MethodStateEnum, PayRefusalEnum, PhoneErrorEnum, SimulatorOutcomeEnum, SupportChannelEnum } from './checkout.enum.js';
|
|
2
2
|
import type { AttemptStatusEnum, CountryEnum, CurrencyEnum, FailureReasonEnum, LangEnum, ProviderLogoEnum } from './domain.enum.js';
|
|
3
3
|
export interface CheckoutApp {
|
|
4
4
|
name: string;
|
|
@@ -186,6 +186,11 @@ export interface CheckoutUi {
|
|
|
186
186
|
/** An attempt the payer walked away from: the form shows again. */
|
|
187
187
|
dismissed: string | null;
|
|
188
188
|
redirecting: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Pay pressed with no method picked, counted: the list turns red and
|
|
191
|
+
* shakes once per press. Back to 0 once a method is picked.
|
|
192
|
+
*/
|
|
193
|
+
methodMissing: number;
|
|
189
194
|
}
|
|
190
195
|
/** Everything the screens show that follows from the payment and the page, worked out once. */
|
|
191
196
|
export interface CheckoutDerived {
|
|
@@ -231,6 +236,13 @@ export interface CheckoutCoreActions {
|
|
|
231
236
|
simulate: (outcome: SimulatorOutcomeEnum) => void;
|
|
232
237
|
}
|
|
233
238
|
/** The checkout's state and actions, the same on the web page and in the payment sheet. */
|
|
239
|
+
/** One short message over the page. */
|
|
240
|
+
export interface CheckoutToast {
|
|
241
|
+
message: string;
|
|
242
|
+
tone: CheckoutToastToneEnum;
|
|
243
|
+
/** The method it is about, shown in place of the tone's icon. */
|
|
244
|
+
logo: ProviderLogoEnum | null;
|
|
245
|
+
}
|
|
234
246
|
export interface CheckoutCore {
|
|
235
247
|
/** Null for a link that matches no payment. */
|
|
236
248
|
view: CheckoutView | null;
|
|
@@ -242,7 +254,7 @@ export interface CheckoutCore {
|
|
|
242
254
|
/** Seconds before the success screen sends the payer back, null once stopped. */
|
|
243
255
|
returnLeft: number | null;
|
|
244
256
|
helpOpen: boolean;
|
|
245
|
-
|
|
257
|
+
toast: CheckoutToast | null;
|
|
246
258
|
copied: boolean;
|
|
247
259
|
/** The Branding tab's preview: nothing can be paid. */
|
|
248
260
|
preview: boolean;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { SheetButtonProps } from '../models/sheet.interface.js';
|
|
2
|
-
/** The sheet's buttons: filled in the app colour for the main action, outlined for the rest. */
|
|
2
|
+
/** The sheet's buttons: filled in the app colour for the main action, outlined for the rest, plain (no border) for a quiet second choice. */
|
|
3
3
|
export declare function SheetButton({ label, onPress, variant, busy, disabled, inactive, }: SheetButtonProps): import("react").JSX.Element;
|
|
@@ -4,7 +4,8 @@ import { CheckoutButtonVariantEnum } from "../checkout-core/models/checkout.enum
|
|
|
4
4
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
5
5
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
6
6
|
import { SheetText } from "./SheetText.js";
|
|
7
|
-
|
|
7
|
+
import { FIELD_HEIGHT, FIELD_RADIUS } from "../models/sheet.constants.js";
|
|
8
|
+
/** The sheet's buttons: filled in the app colour for the main action, outlined for the rest, plain (no border) for a quiet second choice. */
|
|
8
9
|
export function SheetButton({ label, onPress, variant = CheckoutButtonVariantEnum.PRIMARY, busy = false, disabled = false, inactive = false, }) {
|
|
9
10
|
const { colors, brand } = useSheetTheme();
|
|
10
11
|
const primary = variant === CheckoutButtonVariantEnum.PRIMARY;
|
|
@@ -14,14 +15,17 @@ export function SheetButton({ label, onPress, variant = CheckoutButtonVariantEnu
|
|
|
14
15
|
styles.button,
|
|
15
16
|
primary
|
|
16
17
|
? { backgroundColor: brand }
|
|
17
|
-
:
|
|
18
|
+
: variant !== CheckoutButtonVariantEnum.PLAIN && {
|
|
19
|
+
borderWidth: 1,
|
|
20
|
+
borderColor: colors.line3,
|
|
21
|
+
},
|
|
18
22
|
(disabled || inactive) && styles.disabled,
|
|
19
23
|
], children: [busy && _jsx(ActivityIndicator, { color: ink }), _jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 16, color: ink, style: styles.label, children: label })] }));
|
|
20
24
|
}
|
|
21
25
|
const styles = StyleSheet.create({
|
|
22
26
|
button: {
|
|
23
|
-
minHeight:
|
|
24
|
-
borderRadius:
|
|
27
|
+
minHeight: FIELD_HEIGHT,
|
|
28
|
+
borderRadius: FIELD_RADIUS,
|
|
25
29
|
flexDirection: 'row',
|
|
26
30
|
alignItems: 'center',
|
|
27
31
|
justifyContent: 'center',
|
|
@@ -6,6 +6,7 @@ import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
|
6
6
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
7
7
|
import { SheetLogo } from "./SheetLogo.js";
|
|
8
8
|
import { SheetText } from "./SheetText.js";
|
|
9
|
+
import { FIELD_HEIGHT, FIELD_RADIUS } from "../models/sheet.constants.js";
|
|
9
10
|
/** The method picked, on one row, with Change back to the list: as on the web checkout. */
|
|
10
11
|
export function SheetChosenMethod({ logo, name, sub, onChange, }) {
|
|
11
12
|
const t = useTranslations('checkout');
|
|
@@ -23,10 +24,10 @@ const styles = StyleSheet.create({
|
|
|
23
24
|
flexDirection: 'row',
|
|
24
25
|
alignItems: 'center',
|
|
25
26
|
gap: 12,
|
|
26
|
-
height:
|
|
27
|
+
height: FIELD_HEIGHT,
|
|
27
28
|
marginTop: 12,
|
|
28
29
|
borderWidth: 1,
|
|
29
|
-
borderRadius:
|
|
30
|
+
borderRadius: FIELD_RADIUS,
|
|
30
31
|
paddingLeft: 8,
|
|
31
32
|
paddingRight: 6,
|
|
32
33
|
},
|
|
@@ -7,6 +7,7 @@ import { telHref } from "../checkout-core/models/checkout.phone.utils.js";
|
|
|
7
7
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
8
8
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
9
9
|
import { SheetInput, SheetText } from "./SheetText.js";
|
|
10
|
+
import { FIELD_HEIGHT, FIELD_RADIUS } from "../models/sheet.constants.js";
|
|
10
11
|
/**
|
|
11
12
|
* OTP methods: Dial opens the network's code in the phone dialler for a
|
|
12
13
|
* one-time payment code (on Android; on iOS it says to dial it by hand);
|
|
@@ -49,7 +50,7 @@ export function SheetCodeField({ core }) {
|
|
|
49
50
|
};
|
|
50
51
|
return (_jsxs(View, { style: styles.section, children: [_jsx(SheetText, { weight: FigtreeEnum.BOLD, style: styles.label, children: t('otp_label') }), _jsx(SheetInput, { ref: field, accessibilityLabel: t('otp_label'), keyboardType: "number-pad", textContentType: "oneTimeCode", autoComplete: "sms-otp",
|
|
51
52
|
// iOS's number keypads have no Go key: asking for one adds a bar above them.
|
|
52
|
-
returnKeyType: Platform.OS === 'android' ? 'go' : undefined, maxLength: OTP_MAX_LENGTH, value: ui.code, onChangeText: actions.setCode, onFocus: () => setFocused(true), onBlur: () => setFocused(false), onSubmitEditing: actions.pay, editable: !ui.submitting, style: [styles.input, { borderColor: focused ? brand : colors.line3 }] }), _jsxs(View, { style: [styles.help, { backgroundColor: colors.fill2 }], children: [_jsx(SheetText, { size: 13, color: colors.ink2, style: styles.grow, children: t('otp_help', { method: method.name }) }), _jsxs(TouchableOpacity, { onPress: dial, activeOpacity: 0.7, accessibilityRole: "button", accessibilityLabel: t('sheet.dial', { code }), style: [
|
|
53
|
+
returnKeyType: Platform.OS === 'android' ? 'go' : undefined, maxLength: OTP_MAX_LENGTH, value: ui.code, onChangeText: actions.setCode, onFocus: () => setFocused(true), onBlur: () => setFocused(false), onSubmitEditing: actions.pay, editable: !ui.submitting, style: [styles.input, { borderColor: focused ? brand : colors.line3 }] }), _jsxs(View, { style: [styles.help, { backgroundColor: colors.fill2 }], children: [_jsx(SheetText, { paragraph: true, size: 13, color: colors.ink2, style: styles.grow, children: t('otp_help', { method: method.name }) }), _jsxs(TouchableOpacity, { onPress: dial, activeOpacity: 0.7, accessibilityRole: "button", accessibilityLabel: t('sheet.dial', { code }), style: [
|
|
53
54
|
styles.dial,
|
|
54
55
|
{ borderColor: colors.line3, backgroundColor: colors.surface },
|
|
55
56
|
], children: [_jsx(View, { style: [styles.phone, { borderColor: colors.ink }], children: _jsx(View, { style: [styles.speaker, { backgroundColor: colors.ink }] }) }), _jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 14, children: code })] })] })] }));
|
|
@@ -58,9 +59,9 @@ const styles = StyleSheet.create({
|
|
|
58
59
|
section: { marginTop: 10 },
|
|
59
60
|
label: { marginBottom: 8 },
|
|
60
61
|
input: {
|
|
61
|
-
height:
|
|
62
|
+
height: FIELD_HEIGHT,
|
|
62
63
|
borderWidth: 1,
|
|
63
|
-
borderRadius:
|
|
64
|
+
borderRadius: FIELD_RADIUS,
|
|
64
65
|
paddingHorizontal: 14,
|
|
65
66
|
fontSize: 18,
|
|
66
67
|
letterSpacing: 5,
|
|
@@ -93,7 +93,7 @@ export function SheetContent({ calls, watched, fontsReady, deviceLocale, onClose
|
|
|
93
93
|
currency: CURRENCY_LABEL[view.currency],
|
|
94
94
|
}), busy: busy, inactive: !canPay, onPress: actions.pay }), children: [_jsx(SheetSummary, { view: view }), _jsx(SheetForm, { core: core, phoneRef: phoneRef, cardFields: cardFields })] }));
|
|
95
95
|
}
|
|
96
|
-
return (_jsxs(View, { testID: "payment-sheet", accessibilityViewIsModal: true, onAccessibilityEscape: onClose, style: [styles.sheet, { backgroundColor: colors.surface }], children: [_jsx(SheetHeader, { view: view, loading: loading, onClose: onClose, dragHandlers: dragHandlers }), body, _jsx(SheetToast, {
|
|
96
|
+
return (_jsxs(View, { testID: "payment-sheet", accessibilityViewIsModal: true, onAccessibilityEscape: onClose, style: [styles.sheet, { backgroundColor: colors.surface }], children: [_jsx(SheetHeader, { view: view, loading: loading, onClose: onClose, dragHandlers: dragHandlers }), body, _jsx(SheetToast, { toast: core.toast })] }));
|
|
97
97
|
}
|
|
98
98
|
const styles = StyleSheet.create({
|
|
99
99
|
// As tall as its screen; past the room it has, the screen scrolls.
|
|
@@ -27,12 +27,12 @@ export function SheetFailed({ core }) {
|
|
|
27
27
|
? t('sending')
|
|
28
28
|
: attempt.flow === MethodFlowEnum.OTP
|
|
29
29
|
? t('retry_code')
|
|
30
|
-
: t('retry'), busy: ui.submitting, onPress: actions.retry }), _jsx(SheetButton, { variant: CheckoutButtonVariantEnum.
|
|
30
|
+
: t('retry'), busy: ui.submitting, onPress: actions.retry }), _jsx(SheetButton, { variant: CheckoutButtonVariantEnum.PLAIN, label: suggestion
|
|
31
31
|
? t('try_with', { method: suggestion.name })
|
|
32
|
-
: t('use_another'), onPress: actions.useAnother })] }), children: _jsxs(View, { style: styles.failed, children: [_jsx(SheetIcon, { kind: SheetIconEnum.CROSS, color: colors.danger, background: colors.dangerSoft }), _jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 22, accessibilityRole: "header", style: styles.title, children: t(`failure.${reason}.title`, values) }), _jsx(SheetText, { size: 15, color: colors.inkStrong, children: t(`failure.${reason}.body`, values) }), _jsx(SheetText, { color: colors.ink2, style: styles.next, children: t(`failure.${reason}.next`, values) }), _jsxs(View, { style: [styles.rows, { borderColor: colors.line }], children: [_jsx(SheetRow, { label: t('amount'), children: formatMoney(view.amount, view.currency) }), _jsxs(SheetRow, { label: t('method'), children: [_jsx(SheetLogo, { logo: logoOf(attempt) }), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, children: attempt.methodName })] }), _jsx(SheetRow, { label: t('tries_left'), children: t('tries_value', {
|
|
32
|
+
: t('use_another'), onPress: actions.useAnother })] }), children: _jsxs(View, { style: styles.failed, children: [_jsx(SheetIcon, { kind: SheetIconEnum.CROSS, color: colors.danger, background: colors.dangerSoft }), _jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 22, accessibilityRole: "header", style: styles.title, children: t(`failure.${reason}.title`, values) }), _jsx(SheetText, { paragraph: true, size: 15, color: colors.inkStrong, children: t(`failure.${reason}.body`, values) }), _jsx(SheetText, { paragraph: true, color: colors.ink2, style: styles.next, children: t(`failure.${reason}.next`, values) }), _jsxs(View, { style: [styles.rows, { borderColor: colors.line }], children: [_jsx(SheetRow, { label: t('amount'), children: formatMoney(view.amount, view.currency) }), _jsxs(SheetRow, { label: t('method'), children: [_jsx(SheetLogo, { logo: logoOf(attempt) }), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, children: attempt.methodName })] }), _jsx(SheetRow, { label: t('tries_left'), children: t('tries_value', {
|
|
33
33
|
left: view.attemptsLeft,
|
|
34
34
|
max: view.maxAttempts,
|
|
35
|
-
}) })] }), reason !== FailureReasonEnum.MISMATCH && (_jsx(SheetText, { size: 13, color: colors.muted, style: styles.note, children: t('not_charged') }))] }) }));
|
|
35
|
+
}) })] }), reason !== FailureReasonEnum.MISMATCH && (_jsx(SheetText, { paragraph: true, size: 13, color: colors.muted, style: styles.note, children: t('not_charged') }))] }) }));
|
|
36
36
|
}
|
|
37
37
|
const styles = StyleSheet.create({
|
|
38
38
|
failed: { paddingHorizontal: 18, paddingTop: 28 },
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Pressable, StyleSheet, View } from 'react-native';
|
|
2
|
+
import { Animated, Pressable, StyleSheet, View } from 'react-native';
|
|
3
3
|
import { useTranslations } from 'use-intl';
|
|
4
4
|
import { CheckoutTabEnum, MethodFlowEnum, MethodStateEnum, } from "../checkout-core/models/checkout.enum.js";
|
|
5
5
|
import { logoOf } from "../checkout-core/models/checkout.view.utils.js";
|
|
6
|
+
import { useShake } from "../hooks/use-shake.js";
|
|
6
7
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
7
8
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
8
9
|
import { flagOf } from "../models/sheet.utils.js";
|
|
@@ -12,13 +13,17 @@ import { SheetCodeField } from "./SheetCodeField.js";
|
|
|
12
13
|
import { SheetMethodCard } from "./SheetMethodCard.js";
|
|
13
14
|
import { SheetPhoneField } from "./SheetPhoneField.js";
|
|
14
15
|
import { SheetText } from "./SheetText.js";
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
import { FIELD_HEIGHT, FIELD_RADIUS, TABS_INSET, } from "../models/sheet.constants.js";
|
|
17
|
+
/** Two cards a row, the last one alone when they are odd; red with a hint, and shaken, after Pay with none picked. */
|
|
18
|
+
function MethodGrid({ cards, missing }) {
|
|
19
|
+
const t = useTranslations('checkout');
|
|
20
|
+
const { colors } = useSheetTheme();
|
|
21
|
+
const shake = useShake(missing);
|
|
17
22
|
const rows = [];
|
|
18
23
|
for (let index = 0; index < cards.length; index += 2) {
|
|
19
24
|
rows.push(cards.slice(index, index + 2));
|
|
20
25
|
}
|
|
21
|
-
return (
|
|
26
|
+
return (_jsxs(Animated.View, { testID: "method-grid", accessibilityRole: "radiogroup", style: [styles.grid, { transform: [{ translateX: shake }] }], children: [rows.map((row) => (_jsxs(View, { style: styles.gridRow, children: [row.map(({ id, ...card }) => (_jsx(SheetMethodCard, { ...card, missing: missing > 0 }, id))), row.length === 1 && _jsx(View, { style: styles.gridSpacer })] }, row[0].id))), missing > 0 && (_jsx(SheetText, { accessibilityRole: "alert", weight: FigtreeEnum.MEDIUM, size: 13, color: colors.danger, children: t('method_missing') }))] }));
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* The form of design A: the country when there are several, the Mobile
|
|
@@ -61,7 +66,7 @@ export function SheetForm({ core, phoneRef, cardFields }) {
|
|
|
61
66
|
? { backgroundColor: colors.ink, borderColor: colors.ink }
|
|
62
67
|
: { borderColor: colors.line3 },
|
|
63
68
|
], children: [_jsx(SheetText, { size: 14, children: flagOf(country) }), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, size: 13, color: on ? colors.surface : colors.ink, children: t(`countries.${country}`) })] }, country));
|
|
64
|
-
}) })), _jsxs(View, { accessibilityRole: "tablist", style: [styles.tabs, { backgroundColor: colors.fill }], children: [tab(CheckoutTabEnum.MOBILE_MONEY, t('tab_wallet')), tab(CheckoutTabEnum.CARD, t('card'))] }), derived.selected ? (_jsx(SheetChosenMethod, { logo: logoOf(derived.selected), name: derived.selected.name, sub: derived.selected.network, onChange: actions.openPicker })) : (_jsx(MethodGrid, { cards: cards })), !onCard && _jsx(SheetPhoneField, { core: core, phoneRef: phoneRef }), !onCard && derived.selected?.flow === MethodFlowEnum.OTP && (_jsx(SheetCodeField, { core: core })), onCard && derived.selected?.flow === MethodFlowEnum.CARD_FIELDS && (_jsx(SheetCardFields, { core: core, bridge: cardFields }))] }));
|
|
69
|
+
}) })), _jsxs(View, { testID: "sheet-tabs", accessibilityRole: "tablist", style: [styles.tabs, { backgroundColor: colors.fill }], children: [tab(CheckoutTabEnum.MOBILE_MONEY, t('tab_wallet')), tab(CheckoutTabEnum.CARD, t('card'))] }), derived.selected ? (_jsx(SheetChosenMethod, { logo: logoOf(derived.selected), name: derived.selected.name, sub: derived.selected.network, onChange: actions.openPicker })) : (_jsx(MethodGrid, { cards: cards, missing: ui.methodMissing })), !onCard && _jsx(SheetPhoneField, { core: core, phoneRef: phoneRef }), !onCard && derived.selected?.flow === MethodFlowEnum.OTP && (_jsx(SheetCodeField, { core: core })), onCard && derived.selected?.flow === MethodFlowEnum.CARD_FIELDS && (_jsx(SheetCardFields, { core: core, bridge: cardFields }))] }));
|
|
65
70
|
}
|
|
66
71
|
const styles = StyleSheet.create({
|
|
67
72
|
form: { paddingHorizontal: 18, paddingTop: 14, paddingBottom: 8 },
|
|
@@ -80,13 +85,17 @@ const styles = StyleSheet.create({
|
|
|
80
85
|
paddingHorizontal: 12,
|
|
81
86
|
paddingVertical: 6,
|
|
82
87
|
},
|
|
83
|
-
tabs: {
|
|
88
|
+
tabs: {
|
|
89
|
+
flexDirection: 'row',
|
|
90
|
+
height: FIELD_HEIGHT,
|
|
91
|
+
borderRadius: FIELD_RADIUS,
|
|
92
|
+
padding: TABS_INSET,
|
|
93
|
+
},
|
|
84
94
|
tab: {
|
|
85
95
|
flex: 1,
|
|
86
96
|
alignItems: 'center',
|
|
87
97
|
justifyContent: 'center',
|
|
88
|
-
|
|
89
|
-
borderRadius: 9,
|
|
98
|
+
borderRadius: FIELD_RADIUS - TABS_INSET,
|
|
90
99
|
},
|
|
91
100
|
tabOn: {
|
|
92
101
|
shadowColor: 'black',
|
|
@@ -3,6 +3,7 @@ import { Image, Pressable, StyleSheet, View } from 'react-native';
|
|
|
3
3
|
import { useTranslations } from 'use-intl';
|
|
4
4
|
import { MEDIA_LINE } from "../checkout-core/models/checkout.constants.js";
|
|
5
5
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
6
|
+
import { SHEET_BADGE_RADIUS, SHEET_BADGE_SIZE, } from "../models/sheet.constants.js";
|
|
6
7
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
7
8
|
import { SheetBone } from "./SheetBone.js";
|
|
8
9
|
import { SheetText } from "./SheetText.js";
|
|
@@ -14,8 +15,8 @@ export function SheetHeader({ view, loading, onClose, dragHandlers, }) {
|
|
|
14
15
|
const t = useTranslations('checkout');
|
|
15
16
|
const { colors, brand } = useSheetTheme();
|
|
16
17
|
const app = view?.app;
|
|
17
|
-
return (_jsxs(View, { ...dragHandlers, children: [_jsx(View, { style: [styles.grabber, { backgroundColor: colors.line3 }] }), _jsxs(View, { style: styles.row, children: [loading ? (_jsxs(_Fragment, { children: [_jsx(SheetBone, { width:
|
|
18
|
-
(app.logoUrl ? (_jsx(Image, { source: { uri: app.logoUrl }, accessibilityIgnoresInvertColors: true, style: styles.logo })) : (_jsx(View, { testID: "app-initial", style: [styles.initial, { backgroundColor: brand }], children: _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size:
|
|
18
|
+
return (_jsxs(View, { ...dragHandlers, children: [_jsx(View, { style: [styles.grabber, { backgroundColor: colors.line3 }] }), _jsxs(View, { style: styles.row, children: [loading ? (_jsxs(_Fragment, { children: [_jsx(SheetBone, { width: SHEET_BADGE_SIZE, height: SHEET_BADGE_SIZE, radius: SHEET_BADGE_RADIUS }), _jsx(View, { style: styles.name, children: _jsx(SheetBone, { width: 84, height: 14 }) })] })) : (app &&
|
|
19
|
+
(app.logoUrl ? (_jsx(Image, { testID: "app-logo", source: { uri: app.logoUrl }, accessibilityIgnoresInvertColors: true, style: styles.logo })) : (_jsx(View, { testID: "app-initial", style: [styles.initial, { backgroundColor: brand }], children: _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 18, color: "white", style: styles.centred, children: app.name.charAt(0) }) })))), !loading && (_jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 16, numberOfLines: 1, accessibilityRole: "header", style: [styles.name, styles.centred], children: app?.name ?? 'Brissoto Pay' })), !loading && view?.test && (_jsx(View, { style: [styles.badge, { backgroundColor: colors.warnSoft }], children: _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 10.5, color: colors.warnInk, style: [styles.badgeText, styles.centred], children: t('test_badge') }) })), _jsxs(Pressable, { onPress: onClose, accessibilityRole: "button", accessibilityLabel: t('close'), hitSlop: 8, style: [styles.close, { backgroundColor: colors.fill }], children: [_jsx(View, { style: [styles.bar, styles.barUp, { backgroundColor: colors.ink2 }] }), _jsx(View, { style: [
|
|
19
20
|
styles.bar,
|
|
20
21
|
styles.barDown,
|
|
21
22
|
{ backgroundColor: colors.ink2 },
|
|
@@ -37,16 +38,16 @@ const styles = StyleSheet.create({
|
|
|
37
38
|
paddingTop: 10,
|
|
38
39
|
},
|
|
39
40
|
logo: {
|
|
40
|
-
width:
|
|
41
|
-
height:
|
|
42
|
-
borderRadius:
|
|
41
|
+
width: SHEET_BADGE_SIZE,
|
|
42
|
+
height: SHEET_BADGE_SIZE,
|
|
43
|
+
borderRadius: SHEET_BADGE_RADIUS,
|
|
43
44
|
borderWidth: 1,
|
|
44
45
|
borderColor: MEDIA_LINE,
|
|
45
46
|
},
|
|
46
47
|
initial: {
|
|
47
|
-
width:
|
|
48
|
-
height:
|
|
49
|
-
borderRadius:
|
|
48
|
+
width: SHEET_BADGE_SIZE,
|
|
49
|
+
height: SHEET_BADGE_SIZE,
|
|
50
|
+
borderRadius: SHEET_BADGE_RADIUS,
|
|
50
51
|
alignItems: 'center',
|
|
51
52
|
justifyContent: 'center',
|
|
52
53
|
},
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { SHEET_BADGE_SIZE } from "../models/sheet.constants.js";
|
|
3
4
|
import { SheetIconEnum } from "../models/sheet.enum.js";
|
|
4
5
|
/** A sign in a disc, drawn with views: the sheet ships no icon font or SVG. */
|
|
5
|
-
export function SheetIcon({ kind, color, background, size =
|
|
6
|
+
export function SheetIcon({ kind, color, background, size = SHEET_BADGE_SIZE, }) {
|
|
6
7
|
const scale = size / 48;
|
|
7
8
|
const ink = { backgroundColor: color };
|
|
8
9
|
const line = { borderColor: color };
|
|
@@ -12,7 +13,8 @@ export function SheetIcon({ kind, color, background, size = 48, }) {
|
|
|
12
13
|
sign = _jsx(View, { style: [styles.check, line] });
|
|
13
14
|
break;
|
|
14
15
|
case SheetIconEnum.CROSS:
|
|
15
|
-
|
|
16
|
+
// Crossed in a box of their own length: centred in the disc.
|
|
17
|
+
sign = (_jsxs(View, { style: styles.cross, children: [_jsx(View, { style: [styles.bar, styles.up, ink] }), _jsx(View, { style: [styles.bar, styles.down, ink] })] }));
|
|
16
18
|
break;
|
|
17
19
|
case SheetIconEnum.CLOCK:
|
|
18
20
|
sign = (_jsxs(View, { style: [styles.ring, line], children: [_jsx(View, { style: [styles.hour, ink] }), _jsx(View, { style: [styles.minute, ink] })] }));
|
|
@@ -43,6 +45,12 @@ const styles = StyleSheet.create({
|
|
|
43
45
|
borderBottomWidth: 3,
|
|
44
46
|
transform: [{ rotate: '45deg' }],
|
|
45
47
|
},
|
|
48
|
+
cross: {
|
|
49
|
+
width: 18,
|
|
50
|
+
height: 18,
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
},
|
|
46
54
|
bar: { position: 'absolute', width: 18, height: 2.6, borderRadius: 1.3 },
|
|
47
55
|
up: { transform: [{ rotate: '45deg' }] },
|
|
48
56
|
down: { transform: [{ rotate: '-45deg' }] },
|
|
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
3
3
|
import { Keyboard, Platform, ScrollView, StyleSheet, View } from 'react-native';
|
|
4
4
|
import { useTranslations } from 'use-intl';
|
|
5
5
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
6
|
+
import { SHEET_FOOT_GAP, SHEET_FOOT_TOP } from "../models/sheet.constants.js";
|
|
6
7
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
7
8
|
import { SheetText } from "./SheetText.js";
|
|
8
9
|
/**
|
|
@@ -39,10 +40,11 @@ const styles = StyleSheet.create({
|
|
|
39
40
|
foot: {
|
|
40
41
|
borderTopWidth: 1,
|
|
41
42
|
paddingHorizontal: 18,
|
|
42
|
-
paddingTop:
|
|
43
|
+
paddingTop: SHEET_FOOT_TOP,
|
|
43
44
|
paddingBottom: 28,
|
|
44
|
-
gap:
|
|
45
|
+
gap: SHEET_FOOT_GAP,
|
|
45
46
|
},
|
|
46
47
|
footOnKeyboard: { paddingBottom: 12 },
|
|
47
|
-
|
|
48
|
+
// Android's extra font padding would push it off the middle.
|
|
49
|
+
secure: { textAlign: 'center', includeFontPadding: false },
|
|
48
50
|
});
|
|
@@ -7,7 +7,7 @@ import { SheetText } from "./SheetText.js";
|
|
|
7
7
|
/** A dead end the payer cannot fix here: its sign, what happened and why. */
|
|
8
8
|
export function SheetMessage({ title, body, icon = SheetIconEnum.BAN, danger = false, }) {
|
|
9
9
|
const { colors } = useSheetTheme();
|
|
10
|
-
return (_jsxs(View, { style: styles.message, children: [_jsx(SheetIcon, { kind: icon, color: danger ? colors.danger : colors.ink2, background: danger ? colors.dangerSoft : colors.fill2 }), _jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 22, accessibilityRole: "header", style: styles.title, children: title }), _jsx(SheetText, { size: 15, color: colors.inkStrong, children: body })] }));
|
|
10
|
+
return (_jsxs(View, { style: styles.message, children: [_jsx(SheetIcon, { kind: icon, color: danger ? colors.danger : colors.ink2, background: danger ? colors.dangerSoft : colors.fill2 }), _jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 22, accessibilityRole: "header", style: styles.title, children: title }), _jsx(SheetText, { paragraph: true, size: 15, color: colors.inkStrong, children: body })] }));
|
|
11
11
|
}
|
|
12
12
|
const styles = StyleSheet.create({
|
|
13
13
|
message: { paddingHorizontal: 18, paddingTop: 28 },
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { MethodCardProps } from '../models/sheet.interface.js';
|
|
2
2
|
/** One method of the grid: logo and radio dot, then its name and network, or why it is greyed. Picked, it becomes the chosen row. */
|
|
3
|
-
export declare function SheetMethodCard({ logo, name, sub, muted, onSelect, }: MethodCardProps): import("react").JSX.Element;
|
|
3
|
+
export declare function SheetMethodCard({ logo, name, sub, muted, missing, onSelect, }: MethodCardProps): import("react").JSX.Element;
|
|
@@ -5,12 +5,25 @@ import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
|
5
5
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
6
6
|
import { SheetLogo } from "./SheetLogo.js";
|
|
7
7
|
import { SheetText } from "./SheetText.js";
|
|
8
|
+
import { FIELD_RADIUS } from "../models/sheet.constants.js";
|
|
8
9
|
/** One method of the grid: logo and radio dot, then its name and network, or why it is greyed. Picked, it becomes the chosen row. */
|
|
9
|
-
export function SheetMethodCard({ logo, name, sub, muted, onSelect, }) {
|
|
10
|
+
export function SheetMethodCard({ logo, name, sub, muted, missing = false, onSelect, }) {
|
|
10
11
|
const { colors } = useSheetTheme();
|
|
11
|
-
|
|
12
|
+
const red = missing && !muted;
|
|
13
|
+
return (_jsxs(TouchableOpacity, { onPress: onSelect, disabled: muted, activeOpacity: 0.7, accessibilityRole: "radio", accessibilityLabel: `${name}, ${sub}`, accessibilityState: { checked: false, disabled: muted }, style: [
|
|
14
|
+
styles.card,
|
|
15
|
+
red
|
|
16
|
+
? [styles.cardMissing, { borderColor: colors.danger }]
|
|
17
|
+
: { borderColor: colors.line2 },
|
|
18
|
+
], children: [_jsxs(View, { style: styles.top, children: [_jsx(SheetLogo, { logo: logo, size: LOGO_SIZE.ROW, muted: muted }), _jsx(View, { style: [
|
|
12
19
|
styles.radio,
|
|
13
|
-
{
|
|
20
|
+
{
|
|
21
|
+
borderColor: muted
|
|
22
|
+
? colors.line2
|
|
23
|
+
: red
|
|
24
|
+
? colors.danger
|
|
25
|
+
: colors.line4,
|
|
26
|
+
},
|
|
14
27
|
] })] }), _jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 15, numberOfLines: 1, color: muted ? colors.muted : colors.ink, children: name }), _jsx(SheetText, { size: 12.5, numberOfLines: 1, color: muted ? colors.warnInk : colors.muted, children: sub })] }));
|
|
15
28
|
}
|
|
16
29
|
const styles = StyleSheet.create({
|
|
@@ -18,10 +31,12 @@ const styles = StyleSheet.create({
|
|
|
18
31
|
flex: 1,
|
|
19
32
|
minWidth: 0,
|
|
20
33
|
borderWidth: 1,
|
|
21
|
-
borderRadius:
|
|
34
|
+
borderRadius: FIELD_RADIUS,
|
|
22
35
|
padding: 12,
|
|
23
36
|
gap: 2,
|
|
24
37
|
},
|
|
38
|
+
// 1.5 like the web's ring; the padding gives back the half point.
|
|
39
|
+
cardMissing: { borderWidth: 1.5, padding: 11.5 },
|
|
25
40
|
top: {
|
|
26
41
|
flexDirection: 'row',
|
|
27
42
|
alignItems: 'flex-start',
|
|
@@ -6,6 +6,7 @@ import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
|
6
6
|
import { flagOf } from "../models/sheet.utils.js";
|
|
7
7
|
import { SheetLogo } from "./SheetLogo.js";
|
|
8
8
|
import { SheetInput, SheetText } from "./SheetText.js";
|
|
9
|
+
import { FIELD_HEIGHT, FIELD_RADIUS } from "../models/sheet.constants.js";
|
|
9
10
|
/** The Mobile Money number on the phone keyboard: the country's dial code, the number grouped as typed, its check. */
|
|
10
11
|
export function SheetPhoneField({ core, phoneRef }) {
|
|
11
12
|
const t = useTranslations('checkout');
|
|
@@ -24,7 +25,7 @@ export function SheetPhoneField({ core, phoneRef }) {
|
|
|
24
25
|
},
|
|
25
26
|
], children: [_jsxs(View, { style: [styles.dial, { borderColor: colors.line2 }], children: [_jsx(SheetText, { size: 16, children: flagOf(ui.country) }), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, size: 16, children: `+${phone.dial}` })] }), _jsx(SheetInput, { ref: phoneRef, accessibilityLabel: t('phone_label'), keyboardType: "phone-pad", textContentType: "telephoneNumber", autoComplete: "tel",
|
|
26
27
|
// iOS's number keypads have no Go key: asking for one adds a bar above them.
|
|
27
|
-
returnKeyType: Platform.OS === 'android' ? 'go' : undefined, placeholder: phone.placeholder, value: phone.value, onChangeText: (text) => void phone.type(text, text.length), onFocus: phone.onFocus, onBlur: phone.onBlur, onSubmitEditing: actions.pay, editable: !ui.submitting, style: styles.input })] }), _jsx(View, { accessibilityLiveRegion: "polite", style: styles.message, children: error ? (_jsx(SheetText, { weight: FigtreeEnum.MEDIUM, size: 13, color: colors.danger, children: t(`phone_errors.${error}`) })) : mismatch ? (_jsxs(View, { style: styles.mismatch, children: [_jsx(SheetLogo, { logo: mismatch.logos[0] }), _jsx(SheetText, { size: 13, color: colors.ink2, style: styles.grow, children: t('mismatch', { method: mismatch.name }) }), _jsx(Pressable, { onPress: actions.switchToDetected, accessibilityRole: "button", hitSlop: 6, style: [styles.switch, { borderColor: colors.line3 }], children: _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, size: 13, children: t('switch_to') }) })] })) : (selected && (_jsx(SheetText, { size: 13, color: colors.muted, children: t('helper', { method: selected.name }) }))) })] }));
|
|
28
|
+
returnKeyType: Platform.OS === 'android' ? 'go' : undefined, placeholder: phone.placeholder, value: phone.value, onChangeText: (text) => void phone.type(text, text.length), onFocus: phone.onFocus, onBlur: phone.onBlur, onSubmitEditing: actions.pay, editable: !ui.submitting, style: styles.input })] }), _jsx(View, { accessibilityLiveRegion: "polite", style: styles.message, children: error ? (_jsx(SheetText, { paragraph: true, weight: FigtreeEnum.MEDIUM, size: 13, color: colors.danger, children: t(`phone_errors.${error}`) })) : mismatch ? (_jsxs(View, { style: styles.mismatch, children: [_jsx(SheetLogo, { logo: mismatch.logos[0] }), _jsx(SheetText, { paragraph: true, size: 13, color: colors.ink2, style: styles.grow, children: t('mismatch', { method: mismatch.name }) }), _jsx(Pressable, { onPress: actions.switchToDetected, accessibilityRole: "button", hitSlop: 6, style: [styles.switch, { borderColor: colors.line3 }], children: _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, size: 13, children: t('switch_to') }) })] })) : (selected && (_jsx(SheetText, { paragraph: true, size: 13, color: colors.muted, children: t('helper', { method: selected.name }) }))) })] }));
|
|
28
29
|
}
|
|
29
30
|
const styles = StyleSheet.create({
|
|
30
31
|
section: { marginTop: 18 },
|
|
@@ -32,9 +33,9 @@ const styles = StyleSheet.create({
|
|
|
32
33
|
field: {
|
|
33
34
|
flexDirection: 'row',
|
|
34
35
|
alignItems: 'center',
|
|
35
|
-
height:
|
|
36
|
+
height: FIELD_HEIGHT,
|
|
36
37
|
borderWidth: 1,
|
|
37
|
-
borderRadius:
|
|
38
|
+
borderRadius: FIELD_RADIUS,
|
|
38
39
|
overflow: 'hidden',
|
|
39
40
|
},
|
|
40
41
|
dial: {
|
|
@@ -18,7 +18,7 @@ export function SheetSimulator({ core }) {
|
|
|
18
18
|
return (_jsxs(View, { accessibilityLabel: t('simulator.title'), style: [
|
|
19
19
|
styles.panel,
|
|
20
20
|
{ backgroundColor: colors.warnSoft, borderColor: colors.warnLine },
|
|
21
|
-
], children: [_jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 12, color: colors.warnInk2, style: styles.title, children: t('simulator.title') }), _jsxs(SheetText, { size: 13, color: colors.warnInk2, style: styles.body, children: [t('simulator.body'), traits.prompt &&
|
|
21
|
+
], children: [_jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 12, color: colors.warnInk2, style: styles.title, children: t('simulator.title') }), _jsxs(SheetText, { paragraph: true, size: 13, color: colors.warnInk2, style: styles.body, children: [t('simulator.body'), traits.prompt &&
|
|
22
22
|
` ${t('simulator.no_answer', { suffix: SIMULATOR_NO_ANSWER_SUFFIX })}`] }), _jsx(View, { style: styles.outcomes, children: outcomes.map((outcome) => (_jsx(TouchableOpacity, { onPress: () => core.actions.simulate(outcome), activeOpacity: 0.7, accessibilityRole: "button", style: [
|
|
23
23
|
styles.outcome,
|
|
24
24
|
{ backgroundColor: colors.surface, borderColor: colors.warnLine },
|
|
@@ -26,7 +26,7 @@ export function SheetSuccess({ core, onClose }) {
|
|
|
26
26
|
// Nothing stops the countdown in the sheet: it always runs here.
|
|
27
27
|
, {
|
|
28
28
|
// Nothing stops the countdown in the sheet: it always runs here.
|
|
29
|
-
label: t('sheet.done_in', { seconds: returnLeft }), onPress: onClose }), children: _jsxs(View, { style: styles.success, children: [_jsx(View, { style: styles.icon, children: _jsx(SheetIcon, { kind: SheetIconEnum.CHECK, color: colors.surface, background: colors.successInk, size: 64 }) }), _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 22, accessibilityRole: "header", style: [styles.center, styles.title], children: t('success_title') }), _jsxs(View, { style: styles.amountRow, children: [_jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 34, style: styles.amount, children: formatAmount(view.amount, view.currency) }), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, size: 16, color: colors.ink2, children: CURRENCY_LABEL[view.currency] })] }), _jsx(SheetText, { color: colors.muted, style: styles.center, children: t('paid_to', { app: view.app.name }) }), _jsxs(View, { style: [styles.rows, { borderColor: colors.line }], children: [_jsxs(SheetRow, { label: t('method'), children: [_jsx(SheetLogo, { logo: logoOf(attempt) }), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, children: attempt.methodName })] }), attempt.phone && (_jsx(SheetRow, { label: t('number'), children: maskE164(attempt.phone) })), attempt.card && (_jsx(SheetRow, { label: t('card'), children: cardLabel(attempt.card) })), _jsx(SheetRow, { label: t('date'), children: format.dateTime(view.paidAt, {
|
|
29
|
+
label: t('sheet.done_in', { seconds: returnLeft }), onPress: onClose }), children: _jsxs(View, { style: styles.success, children: [_jsx(View, { style: styles.icon, children: _jsx(SheetIcon, { kind: SheetIconEnum.CHECK, color: colors.surface, background: colors.successInk, size: 64 }) }), _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 22, accessibilityRole: "header", style: [styles.center, styles.title], children: t('success_title') }), _jsxs(View, { style: styles.amountRow, children: [_jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 34, style: styles.amount, children: formatAmount(view.amount, view.currency) }), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, size: 16, color: colors.ink2, children: CURRENCY_LABEL[view.currency] })] }), _jsx(SheetText, { paragraph: true, color: colors.muted, style: styles.center, children: t('paid_to', { app: view.app.name }) }), _jsxs(View, { style: [styles.rows, { borderColor: colors.line }], children: [_jsxs(SheetRow, { label: t('method'), children: [_jsx(SheetLogo, { logo: logoOf(attempt) }), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, children: attempt.methodName })] }), attempt.phone && (_jsx(SheetRow, { label: t('number'), children: maskE164(attempt.phone) })), attempt.card && (_jsx(SheetRow, { label: t('card'), children: cardLabel(attempt.card) })), _jsx(SheetRow, { label: t('date'), children: format.dateTime(view.paidAt, {
|
|
30
30
|
day: 'numeric',
|
|
31
31
|
month: 'short',
|
|
32
32
|
year: 'numeric',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SheetInputProps, SheetTextProps } from '../models/sheet.interface.js';
|
|
2
|
-
/** Every text of the sheet: Figtree, in the sheet's ink unless told otherwise. */
|
|
3
|
-
export declare function SheetText({ weight, size, color, style, ...props }: SheetTextProps): import("react").JSX.Element;
|
|
2
|
+
/** Every text of the sheet: Figtree, in the sheet's ink unless told otherwise; a paragraph's lines spaced as on the web. */
|
|
3
|
+
export declare function SheetText({ weight, size, color, paragraph, style, ...props }: SheetTextProps): import("react").JSX.Element;
|
|
4
4
|
/** Every field of the sheet: Figtree at 16, so iOS never zooms, in the sheet's ink. */
|
|
5
5
|
export declare function SheetInput({ style, ...props }: SheetInputProps): import("react").JSX.Element;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Text, TextInput } from 'react-native';
|
|
3
3
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
4
|
+
import { PARAGRAPH_LINE_HEIGHT } from "../models/sheet.constants.js";
|
|
4
5
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
5
|
-
/** Every text of the sheet: Figtree, in the sheet's ink unless told otherwise. */
|
|
6
|
-
export function SheetText({ weight = FigtreeEnum.REGULAR, size = 14, color, style, ...props }) {
|
|
6
|
+
/** Every text of the sheet: Figtree, in the sheet's ink unless told otherwise; a paragraph's lines spaced as on the web. */
|
|
7
|
+
export function SheetText({ weight = FigtreeEnum.REGULAR, size = 14, color, paragraph = false, style, ...props }) {
|
|
7
8
|
const { colors } = useSheetTheme();
|
|
8
9
|
return (_jsx(Text, { ...props, style: [
|
|
9
10
|
{ fontFamily: weight, fontSize: size, color: color ?? colors.ink },
|
|
11
|
+
paragraph && { lineHeight: Math.round(size * PARAGRAPH_LINE_HEIGHT) },
|
|
10
12
|
style,
|
|
11
13
|
] }));
|
|
12
14
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { SheetToastProps } from '../models/sheet.interface.js';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* One short message at a time, in a small card at the top of the sheet:
|
|
4
|
+
* the method's logo, else the tone's disc (a check when done, "!" for a
|
|
5
|
+
* problem, "i" to know). As on the web checkout.
|
|
6
|
+
*/
|
|
7
|
+
export declare function SheetToast({ toast }: SheetToastProps): import("react").JSX.Element | null;
|
|
@@ -1,24 +1,71 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { CheckoutToastToneEnum } from "../checkout-core/models/checkout.enum.js";
|
|
3
4
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
4
5
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
6
|
+
import { SheetLogo } from "./SheetLogo.js";
|
|
5
7
|
import { SheetText } from "./SheetText.js";
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* One short message at a time, in a small card at the top of the sheet:
|
|
10
|
+
* the method's logo, else the tone's disc (a check when done, "!" for a
|
|
11
|
+
* problem, "i" to know). As on the web checkout.
|
|
12
|
+
*/
|
|
13
|
+
export function SheetToast({ toast }) {
|
|
14
|
+
const { colors, dark } = useSheetTheme();
|
|
15
|
+
if (!toast)
|
|
10
16
|
return null;
|
|
11
|
-
|
|
17
|
+
const disc = {
|
|
18
|
+
[CheckoutToastToneEnum.SUCCESS]: colors.successInk,
|
|
19
|
+
[CheckoutToastToneEnum.WARNING]: colors.warnInk,
|
|
20
|
+
[CheckoutToastToneEnum.INFO]: colors.muted,
|
|
21
|
+
}[toast.tone];
|
|
22
|
+
return (_jsx(View, { pointerEvents: "none", style: styles.place, children: _jsxs(View, { testID: "sheet-toast", accessibilityLiveRegion: "polite", accessibilityRole: "alert", style: [
|
|
23
|
+
styles.toast,
|
|
24
|
+
{
|
|
25
|
+
backgroundColor: dark ? colors.fill3 : colors.surface,
|
|
26
|
+
borderColor: colors.line3,
|
|
27
|
+
},
|
|
28
|
+
], children: [toast.logo ? (_jsx(SheetLogo, { logo: toast.logo })) : (_jsx(View, { testID: `toast-${toast.tone}`, style: [styles.disc, { backgroundColor: disc }], children: toast.tone === CheckoutToastToneEnum.SUCCESS ? (_jsx(View, { style: styles.check })) : (_jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 12, color: "white", style: styles.glyph, children: toast.tone === CheckoutToastToneEnum.WARNING ? '!' : 'i' })) })), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, size: 13.5, style: styles.text, children: toast.message })] }) }));
|
|
12
29
|
}
|
|
13
30
|
const styles = StyleSheet.create({
|
|
14
|
-
|
|
31
|
+
place: {
|
|
15
32
|
position: 'absolute',
|
|
16
|
-
top:
|
|
33
|
+
top: 12,
|
|
17
34
|
left: 18,
|
|
18
35
|
right: 18,
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
},
|
|
38
|
+
toast: {
|
|
39
|
+
flexDirection: 'row',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
gap: 10,
|
|
42
|
+
borderWidth: 1,
|
|
19
43
|
borderRadius: 12,
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
paddingVertical: 10,
|
|
45
|
+
paddingLeft: 12,
|
|
46
|
+
paddingRight: 16,
|
|
47
|
+
shadowColor: 'black',
|
|
48
|
+
shadowOpacity: 0.18,
|
|
49
|
+
shadowRadius: 16,
|
|
50
|
+
shadowOffset: { width: 0, height: 8 },
|
|
51
|
+
elevation: 6,
|
|
52
|
+
},
|
|
53
|
+
disc: {
|
|
54
|
+
width: 20,
|
|
55
|
+
height: 20,
|
|
56
|
+
borderRadius: 10,
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
},
|
|
60
|
+
check: {
|
|
61
|
+
width: 5,
|
|
62
|
+
height: 9,
|
|
63
|
+
marginTop: -2,
|
|
64
|
+
borderColor: 'white',
|
|
65
|
+
borderRightWidth: 2,
|
|
66
|
+
borderBottomWidth: 2,
|
|
67
|
+
transform: [{ rotate: '45deg' }],
|
|
22
68
|
},
|
|
23
|
-
|
|
69
|
+
glyph: { lineHeight: 14, textAlign: 'center' },
|
|
70
|
+
text: { flexShrink: 1 },
|
|
24
71
|
});
|
|
@@ -39,7 +39,7 @@ export function SheetWaiting({ core }) {
|
|
|
39
39
|
: t('resend'), disabled: resendLeft > 0, onPress: () => {
|
|
40
40
|
setSentAt(WAIT_SECONDS - waitLeft);
|
|
41
41
|
actions.resend();
|
|
42
|
-
} })), _jsx(Pressable, { onPress: actions.useAnother, accessibilityRole: "button", style: styles.link, children: _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, style: styles.center, children: t('use_another') }) })] }), children: _jsxs(View, { style: styles.waiting, children: [_jsxs(View, { style: styles.pulse, children: [_jsx(View, { style: [styles.halo, styles.outer, { backgroundColor: brand }] }), _jsx(View, { style: [styles.halo, styles.inner, { backgroundColor: brand }] }), _jsx(View, { testID: "icon-phone", style: [styles.phone, { borderColor: brand }], children: _jsx(View, { style: [styles.speaker, { backgroundColor: brand }] }) })] }), _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 21, accessibilityRole: "header", style: [styles.center, styles.title], children: prompt ? t('sheet.approve_title') : t('checking_title') }), _jsx(SheetText, { size: 14, color: colors.muted, style: styles.center, children: attempt.phone
|
|
42
|
+
} })), _jsx(Pressable, { onPress: actions.useAnother, accessibilityRole: "button", style: styles.link, children: _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, style: styles.center, children: t('use_another') }) })] }), children: _jsxs(View, { style: styles.waiting, children: [_jsxs(View, { style: styles.pulse, children: [_jsx(View, { style: [styles.halo, styles.outer, { backgroundColor: brand }] }), _jsx(View, { style: [styles.halo, styles.inner, { backgroundColor: brand }] }), _jsx(View, { testID: "icon-phone", style: [styles.phone, { borderColor: brand }], children: _jsx(View, { style: [styles.speaker, { backgroundColor: brand }] }) })] }), _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 21, accessibilityRole: "header", style: [styles.center, styles.title], children: prompt ? t('sheet.approve_title') : t('checking_title') }), _jsx(SheetText, { paragraph: true, size: 14, color: colors.muted, style: styles.center, children: attempt.phone
|
|
43
43
|
? prompt
|
|
44
44
|
? t('sheet.approve_body', {
|
|
45
45
|
method: attempt.methodName,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Animated, Easing } from 'react-native';
|
|
3
|
+
import { SHAKE_MS, SHAKE_OFFSETS } from "../models/sheet.constants.js";
|
|
4
|
+
/** A side-to-side shake each time `count` grows past 0: the offset to put in a `translateX`. */
|
|
5
|
+
export function useShake(count) {
|
|
6
|
+
const [offset] = useState(() => new Animated.Value(0));
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (count === 0)
|
|
9
|
+
return;
|
|
10
|
+
Animated.sequence(SHAKE_OFFSETS.map((toValue) => Animated.timing(offset, {
|
|
11
|
+
toValue,
|
|
12
|
+
duration: SHAKE_MS / SHAKE_OFFSETS.length,
|
|
13
|
+
easing: Easing.out(Easing.quad),
|
|
14
|
+
useNativeDriver: true,
|
|
15
|
+
}))).start();
|
|
16
|
+
}, [count, offset]);
|
|
17
|
+
return offset;
|
|
18
|
+
}
|
|
@@ -37,3 +37,26 @@ export declare const SHEET_SWIPE_DISTANCE = 120;
|
|
|
37
37
|
export declare const SHEET_FLICK_SPEED = 0.8;
|
|
38
38
|
/** How the sheet's height eases from one screen to the next. */
|
|
39
39
|
export declare const SHEET_RESIZE: LayoutAnimationConfig;
|
|
40
|
+
/** A paragraph's line height, as a share of its size: the web checkout's 1.5, a touch tighter for Figtree on phones. */
|
|
41
|
+
export declare const PARAGRAPH_LINE_HEIGHT = 1.45;
|
|
42
|
+
/** A field the payer still has to fill, shaken once when Pay is pressed: its offsets, then back. */
|
|
43
|
+
export declare const SHAKE_MS = 300;
|
|
44
|
+
export declare const SHAKE_OFFSETS: number[];
|
|
45
|
+
/** The form's one row height and one corner, Pay's: the tabs, the method picked, the fields and the buttons line up. */
|
|
46
|
+
export declare const FIELD_HEIGHT = 50;
|
|
47
|
+
export declare const FIELD_RADIUS = 14;
|
|
48
|
+
/** The tabs' inset around the tab on: its corner follows the bar's. */
|
|
49
|
+
export declare const TABS_INSET = 3;
|
|
50
|
+
/**
|
|
51
|
+
* Over and under the secured line, between the foot's separator and Pay.
|
|
52
|
+
* Figtree leaves 2pt more room over its letters than under them at this
|
|
53
|
+
* size, so the space over is 2 less: the letters sit in the middle.
|
|
54
|
+
*/
|
|
55
|
+
export declare const SHEET_FOOT_TOP = 11;
|
|
56
|
+
export declare const SHEET_FOOT_GAP = 13;
|
|
57
|
+
/**
|
|
58
|
+
* The app's logo in the header and a screen's disc under it (failed, closed)
|
|
59
|
+
* share one size: one under the other, their centres line up.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SHEET_BADGE_SIZE = 44;
|
|
62
|
+
export declare const SHEET_BADGE_RADIUS = 12;
|
|
@@ -76,3 +76,26 @@ export const SHEET_RESIZE = {
|
|
|
76
76
|
duration: 220,
|
|
77
77
|
update: { type: LayoutAnimation.Types.easeInEaseOut },
|
|
78
78
|
};
|
|
79
|
+
/** A paragraph's line height, as a share of its size: the web checkout's 1.5, a touch tighter for Figtree on phones. */
|
|
80
|
+
export const PARAGRAPH_LINE_HEIGHT = 1.45;
|
|
81
|
+
/** A field the payer still has to fill, shaken once when Pay is pressed: its offsets, then back. */
|
|
82
|
+
export const SHAKE_MS = 300;
|
|
83
|
+
export const SHAKE_OFFSETS = [-6, 6, -3, 0];
|
|
84
|
+
/** The form's one row height and one corner, Pay's: the tabs, the method picked, the fields and the buttons line up. */
|
|
85
|
+
export const FIELD_HEIGHT = 50;
|
|
86
|
+
export const FIELD_RADIUS = 14;
|
|
87
|
+
/** The tabs' inset around the tab on: its corner follows the bar's. */
|
|
88
|
+
export const TABS_INSET = 3;
|
|
89
|
+
/**
|
|
90
|
+
* Over and under the secured line, between the foot's separator and Pay.
|
|
91
|
+
* Figtree leaves 2pt more room over its letters than under them at this
|
|
92
|
+
* size, so the space over is 2 less: the letters sit in the middle.
|
|
93
|
+
*/
|
|
94
|
+
export const SHEET_FOOT_TOP = 11;
|
|
95
|
+
export const SHEET_FOOT_GAP = 13;
|
|
96
|
+
/**
|
|
97
|
+
* The app's logo in the header and a screen's disc under it (failed, closed)
|
|
98
|
+
* share one size: one under the other, their centres line up.
|
|
99
|
+
*/
|
|
100
|
+
export const SHEET_BADGE_SIZE = 44;
|
|
101
|
+
export const SHEET_BADGE_RADIUS = 12;
|
|
@@ -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, CheckoutMutations, CheckoutPlatform, CheckoutView } from '../checkout-core/models/checkout.interface.js';
|
|
6
|
+
import type { CardFieldsMessage, CheckoutColors, CheckoutCore, CheckoutToast, CheckoutMutations, 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
|
|
@@ -88,6 +88,8 @@ export interface SheetTextProps extends TextProps {
|
|
|
88
88
|
weight?: FigtreeEnum;
|
|
89
89
|
size?: number;
|
|
90
90
|
color?: string;
|
|
91
|
+
/** Text that may wrap: lines spaced as on the web checkout. */
|
|
92
|
+
paragraph?: boolean;
|
|
91
93
|
style?: StyleProp<TextStyle>;
|
|
92
94
|
}
|
|
93
95
|
export interface SheetHeaderProps {
|
|
@@ -177,6 +179,8 @@ export interface MethodCardProps {
|
|
|
177
179
|
sub: string;
|
|
178
180
|
/** Greyed and not pressable: in maintenance, or not offered yet. */
|
|
179
181
|
muted: boolean;
|
|
182
|
+
/** Pay was pressed with no method picked: the card is ringed in red. */
|
|
183
|
+
missing?: boolean;
|
|
180
184
|
onSelect?: () => void;
|
|
181
185
|
}
|
|
182
186
|
export interface ChosenMethodProps {
|
|
@@ -192,6 +196,8 @@ export interface MethodGridCard extends MethodCardProps {
|
|
|
192
196
|
}
|
|
193
197
|
export interface MethodGridProps {
|
|
194
198
|
cards: Array<MethodGridCard>;
|
|
199
|
+
/** Pay presses with no method picked: red, and a shake per press. */
|
|
200
|
+
missing: number;
|
|
195
201
|
}
|
|
196
202
|
export interface SheetIconProps {
|
|
197
203
|
kind: SheetIconEnum;
|
|
@@ -209,7 +215,7 @@ export interface SheetRowProps {
|
|
|
209
215
|
urgent?: boolean;
|
|
210
216
|
}
|
|
211
217
|
export interface SheetToastProps {
|
|
212
|
-
|
|
218
|
+
toast: CheckoutToast | null;
|
|
213
219
|
}
|
|
214
220
|
export interface BoneProps {
|
|
215
221
|
/** Points, or a share of the row (`100%`). */
|