@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.
Files changed (37) hide show
  1. package/dist/checkout-core/hooks/use-checkout-controller.js +25 -21
  2. package/dist/checkout-core/hooks/use-checkout-timers.d.ts +6 -3
  3. package/dist/checkout-core/hooks/use-checkout-timers.js +6 -6
  4. package/dist/checkout-core/messages/en.d.ts +1 -0
  5. package/dist/checkout-core/messages/en.js +1 -0
  6. package/dist/checkout-core/messages/fr.js +1 -0
  7. package/dist/checkout-core/models/checkout.enum.d.ts +8 -0
  8. package/dist/checkout-core/models/checkout.enum.js +9 -0
  9. package/dist/checkout-core/models/checkout.interface.d.ts +14 -2
  10. package/dist/checkout-core/models/checkout.view.utils.js +1 -0
  11. package/dist/components/SheetButton.d.ts +1 -1
  12. package/dist/components/SheetButton.js +8 -4
  13. package/dist/components/SheetChosenMethod.js +3 -2
  14. package/dist/components/SheetCodeField.js +4 -3
  15. package/dist/components/SheetContent.js +1 -1
  16. package/dist/components/SheetFailed.js +3 -3
  17. package/dist/components/SheetForm.js +17 -8
  18. package/dist/components/SheetHeader.js +9 -8
  19. package/dist/components/SheetIcon.js +11 -3
  20. package/dist/components/SheetLayout.js +5 -3
  21. package/dist/components/SheetMessage.js +1 -1
  22. package/dist/components/SheetMethodCard.d.ts +1 -1
  23. package/dist/components/SheetMethodCard.js +19 -4
  24. package/dist/components/SheetPhoneField.js +4 -3
  25. package/dist/components/SheetSimulator.js +1 -1
  26. package/dist/components/SheetSuccess.js +1 -1
  27. package/dist/components/SheetText.d.ts +2 -2
  28. package/dist/components/SheetText.js +4 -2
  29. package/dist/components/SheetToast.d.ts +6 -2
  30. package/dist/components/SheetToast.js +58 -11
  31. package/dist/components/SheetWaiting.js +1 -1
  32. package/dist/hooks/use-shake.d.ts +3 -0
  33. package/dist/hooks/use-shake.js +18 -0
  34. package/dist/models/sheet.constants.d.ts +23 -0
  35. package/dist/models/sheet.constants.js +23 -0
  36. package/dist/models/sheet.interface.d.ts +8 -2
  37. 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) => toast.show(result.ok
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
- toastMessage: toast.message,
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
- message: string | null;
14
- show: (next: string) => void;
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 [message, setMessage] = useState(null);
35
+ const [toast, setToast] = useState(null);
36
36
  const timer = useRef(undefined);
37
37
  useEffect(() => () => clearTimeout(timer.current), []);
38
- const show = useCallback((next) => {
38
+ const show = useCallback((message, tone, logo = null) => {
39
39
  clearTimeout(timer.current);
40
- setMessage(next);
41
- timer.current = setTimeout(() => setMessage(null), TOAST_MS);
40
+ setToast({ message, tone, logo });
41
+ timer.current = setTimeout(() => setToast(null), TOAST_MS);
42
42
  }, []);
43
- return { message, show };
43
+ return { toast, show };
44
44
  }
@@ -63,6 +63,7 @@ declare const _default: {
63
63
  };
64
64
  country: string;
65
65
  change: string;
66
+ method_missing: string;
66
67
  secured_by: string;
67
68
  help: string;
68
69
  terms: string;
@@ -54,6 +54,7 @@ export default {
54
54
  },
55
55
  country: 'Country',
56
56
  change: 'Change',
57
+ method_missing: 'Choose a payment method',
57
58
  secured_by: 'Payment secured by',
58
59
  help: 'Need help?',
59
60
  terms: 'Terms',
@@ -55,6 +55,7 @@ const messages = {
55
55
  },
56
56
  country: 'Pays',
57
57
  change: 'Modifier',
58
+ method_missing: 'Choisissez un moyen de paiement',
58
59
  secured_by: 'Paiement sécurisé par',
59
60
  help: "Besoin d'aide ?",
60
61
  terms: 'Conditions',
@@ -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
- toastMessage: string | null;
257
+ toast: CheckoutToast | null;
246
258
  copied: boolean;
247
259
  /** The Branding tab's preview: nothing can be paid. */
248
260
  preview: boolean;
@@ -52,6 +52,7 @@ export function initialUi(view, clues) {
52
52
  submitting: false,
53
53
  dismissed: null,
54
54
  redirecting: false,
55
+ methodMissing: 0,
55
56
  };
56
57
  if (preferred?.kind !== MethodKindEnum.MOBILE_MONEY)
57
58
  return base;
@@ -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
- /** The sheet's buttons: filled in the app colour for the main action, outlined for the rest. */
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
- : { borderWidth: 1, borderColor: colors.line3 },
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: 50,
24
- borderRadius: 14,
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: 48,
27
+ height: FIELD_HEIGHT,
27
28
  marginTop: 12,
28
29
  borderWidth: 1,
29
- borderRadius: 12,
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: 50,
62
+ height: FIELD_HEIGHT,
62
63
  borderWidth: 1,
63
- borderRadius: 13,
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, { message: core.toastMessage })] }));
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.SECONDARY, label: suggestion
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
- /** Two cards a row, the last one alone when they are odd. */
16
- function MethodGrid({ cards }) {
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 (_jsx(View, { accessibilityRole: "radiogroup", style: styles.grid, children: rows.map((row) => (_jsxs(View, { style: styles.gridRow, children: [row.map(({ id, ...card }) => (_jsx(SheetMethodCard, { ...card }, id))), row.length === 1 && _jsx(View, { style: styles.gridSpacer })] }, row[0].id))) }));
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: { flexDirection: 'row', borderRadius: 12, padding: 3 },
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
- minHeight: 36,
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: 34, height: 34, radius: 9 }), _jsx(View, { style: styles.name, children: _jsx(SheetBone, { width: 84, height: 14 }) })] })) : (app &&
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: 15, 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: [
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: 34,
41
- height: 34,
42
- borderRadius: 9,
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: 34,
48
- height: 34,
49
- borderRadius: 9,
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, Fragment as _Fragment, jsxs as _jsxs } 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 { 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 = 48, }) {
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
- sign = (_jsxs(_Fragment, { children: [_jsx(View, { style: [styles.bar, styles.up, ink] }), _jsx(View, { style: [styles.bar, styles.down, ink] })] }));
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: 12,
43
+ paddingTop: SHEET_FOOT_TOP,
43
44
  paddingBottom: 28,
44
- gap: 10,
45
+ gap: SHEET_FOOT_GAP,
45
46
  },
46
47
  footOnKeyboard: { paddingBottom: 12 },
47
- secure: { textAlign: 'center' },
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
- return (_jsxs(TouchableOpacity, { onPress: onSelect, disabled: muted, activeOpacity: 0.7, accessibilityRole: "radio", accessibilityLabel: `${name}, ${sub}`, accessibilityState: { checked: false, disabled: muted }, style: [styles.card, { borderColor: colors.line2 }], children: [_jsxs(View, { style: styles.top, children: [_jsx(SheetLogo, { logo: logo, size: LOGO_SIZE.ROW, muted: muted }), _jsx(View, { style: [
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
- { borderColor: muted ? colors.line2 : colors.line4 },
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: 14,
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: 50,
36
+ height: FIELD_HEIGHT,
36
37
  borderWidth: 1,
37
- borderRadius: 13,
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
- /** One short message at a time, over the top of the sheet. */
3
- export declare function SheetToast({ message }: SheetToastProps): import("react").JSX.Element | null;
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
- /** One short message at a time, over the top of the sheet. */
7
- export function SheetToast({ message }) {
8
- const { colors } = useSheetTheme();
9
- if (!message)
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
- return (_jsx(View, { pointerEvents: "none", accessibilityLiveRegion: "polite", accessibilityRole: "alert", style: [styles.toast, { backgroundColor: colors.ink }], children: _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, size: 13.5, color: colors.surface, style: styles.text, children: message }) }));
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
- toast: {
31
+ place: {
15
32
  position: 'absolute',
16
- top: 56,
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
- paddingHorizontal: 14,
21
- paddingVertical: 11,
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
- text: { textAlign: 'center' },
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,3 @@
1
+ import { Animated } from 'react-native';
2
+ /** A side-to-side shake each time `count` grows past 0: the offset to put in a `translateX`. */
3
+ export declare function useShake(count: number): Animated.Value;
@@ -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
- message: string | null;
218
+ toast: CheckoutToast | null;
213
219
  }
214
220
  export interface BoneProps {
215
221
  /** Points, or a share of the row (`100%`). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyama/pay-react-native",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Brissoto Pay for Expo and React Native apps: the payment sheet.",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,