@heyama/pay-react-native 1.0.1 → 1.0.2

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.
@@ -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. */
@@ -84,7 +84,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
84
84
  if (!waiting || waitLeft > 0)
85
85
  return;
86
86
  update({ dismissed: attemptReference });
87
- showToast(noAnswer);
87
+ showToast(noAnswer, CheckoutToastToneEnum.WARNING);
88
88
  }, [waiting, waitLeft, attemptReference, noAnswer, showToast, update]);
89
89
  const paymentId = view?.id;
90
90
  // A payer opened the link, said once per page (the dashboard's preview has no mutations).
@@ -131,7 +131,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
131
131
  break;
132
132
  case CardFieldsMessageEnum.REFUSED:
133
133
  update({ cardPaying: false });
134
- showToast(message.reason ? refusals(message.reason) : failedToast);
134
+ showToast(message.reason ? refusals(message.reason) : failedToast, CheckoutToastToneEnum.WARNING);
135
135
  break;
136
136
  }
137
137
  };
@@ -139,7 +139,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
139
139
  }, [update, showToast, refusals, failedToast, platform]);
140
140
  /* ---------------------------------------------------------- helpers */
141
141
  const reference = attempt?.reference ?? view?.id ?? '';
142
- const comingSoon = (name) => toast.show(t('toast.soon', { method: name }));
142
+ const comingSoon = (name) => toast.show(t('toast.soon', { method: name }), CheckoutToastToneEnum.INFO);
143
143
  /**
144
144
  * Starts an attempt with the number, and the payment code of an OTP
145
145
  * method; a second tap while one is on its way does nothing.
@@ -155,9 +155,9 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
155
155
  : mutations.payWithCode({ ...args, otp }))
156
156
  .then((result) => {
157
157
  if (!result.ok)
158
- toast.show(t(`refusals.${result.reason}`));
158
+ toast.show(t(`refusals.${result.reason}`), CheckoutToastToneEnum.WARNING);
159
159
  })
160
- .catch(() => toast.show(t('toast.failed')))
160
+ .catch(() => toast.show(t('toast.failed'), CheckoutToastToneEnum.WARNING))
161
161
  .finally(() => {
162
162
  paying.current = false;
163
163
  update({ submitting: false });
@@ -188,7 +188,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
188
188
  comingSoon(method.name);
189
189
  }
190
190
  else if (method.state === MethodStateEnum.MAINTENANCE) {
191
- toast.show(method.note ?? t('toast.unavailable', { method: method.name }));
191
+ toast.show(method.note ?? t('toast.unavailable', { method: method.name }), CheckoutToastToneEnum.WARNING, logoOf(method));
192
192
  }
193
193
  else {
194
194
  update({
@@ -205,7 +205,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
205
205
  switchToDetected: () => {
206
206
  const detected = derived.mismatch;
207
207
  update({ method: detected.id });
208
- toast.show(t('toast.switched', { method: detected.name }));
208
+ toast.show(t('toast.switched', { method: detected.name }), CheckoutToastToneEnum.SUCCESS, logoOf(detected));
209
209
  },
210
210
  pay: () => {
211
211
  const { selected } = derived;
@@ -225,11 +225,11 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
225
225
  }
226
226
  const otp = selected.flow === MethodFlowEnum.OTP;
227
227
  if (otp && !OTP_PATTERN.test(ui.code)) {
228
- toast.show(t(`refusals.${PayRefusalEnum.CODE}`));
228
+ toast.show(t(`refusals.${PayRefusalEnum.CODE}`), CheckoutToastToneEnum.WARNING);
229
229
  return;
230
230
  }
231
231
  if (!mutations) {
232
- toast.show(t('toast.preview'));
232
+ toast.show(t('toast.preview'), CheckoutToastToneEnum.INFO);
233
233
  return;
234
234
  }
235
235
  submit(selected.id, {
@@ -240,7 +240,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
240
240
  // Only offered while an OTP method, which always has its code, is picked.
241
241
  cannotDial: () => {
242
242
  const method = derived.selected;
243
- toast.show(t('toast.dial', { code: method.otpCode, method: method.name }));
243
+ toast.show(t('toast.dial', { code: method.otpCode, method: method.name }), CheckoutToastToneEnum.INFO, logoOf(method));
244
244
  },
245
245
  // Only on the failure screen, which the preview never shows.
246
246
  retry: () => {
@@ -273,15 +273,15 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
273
273
  resend: () => {
274
274
  void mutations
275
275
  .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')));
276
+ .then((result) => result.ok
277
+ ? toast.show(t('toast.resent', { phone: maskE164(attempt.phone) }), CheckoutToastToneEnum.SUCCESS)
278
+ : toast.show(t('toast.resend_wait'), CheckoutToastToneEnum.INFO))
279
+ .catch(() => toast.show(t('toast.failed'), CheckoutToastToneEnum.WARNING));
280
280
  },
281
281
  cancel: () => {
282
282
  void mutations
283
283
  .cancel({ paymentId: view.id })
284
- .catch(() => toast.show(t('toast.failed')));
284
+ .catch(() => toast.show(t('toast.failed'), CheckoutToastToneEnum.WARNING));
285
285
  },
286
286
  returnNow: goBack,
287
287
  stayHere: countdown.stop,
@@ -292,11 +292,11 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
292
292
  clearTimeout(copiedTimer.current);
293
293
  setCopied(true);
294
294
  copiedTimer.current = setTimeout(() => setCopied(false), COPIED_MS);
295
- toast.show(t('toast.copied'));
295
+ toast.show(t('toast.copied'), CheckoutToastToneEnum.SUCCESS);
296
296
  },
297
297
  downloadReceipt: () => {
298
298
  platform.saveReceipt(`receipt-${reference}.txt`, receiptText(view));
299
- toast.show(t('toast.downloaded'));
299
+ toast.show(t('toast.downloaded'), CheckoutToastToneEnum.SUCCESS);
300
300
  },
301
301
  simulate: (outcome) => {
302
302
  void mutations.simulate({ paymentId: view.id, outcome });
@@ -310,7 +310,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
310
310
  waitLeft,
311
311
  returnLeft: countdown.left,
312
312
  helpOpen,
313
- toastMessage: toast.message,
313
+ toast: toast.toast,
314
314
  copied,
315
315
  preview: !mutations,
316
316
  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
  }
@@ -131,3 +131,9 @@ export declare enum CheckoutThemeEnum {
131
131
  LIGHT = "light",
132
132
  DARK = "dark"
133
133
  }
134
+ /** What a toast says: done, a problem, or just good to know. Sets its icon. */
135
+ export declare enum CheckoutToastToneEnum {
136
+ SUCCESS = "success",
137
+ WARNING = "warning",
138
+ INFO = "info"
139
+ }
@@ -148,3 +148,10 @@ export var CheckoutThemeEnum;
148
148
  CheckoutThemeEnum["LIGHT"] = "light";
149
149
  CheckoutThemeEnum["DARK"] = "dark";
150
150
  })(CheckoutThemeEnum || (CheckoutThemeEnum = {}));
151
+ /** What a toast says: done, a problem, or just good to know. Sets its icon. */
152
+ export var CheckoutToastToneEnum;
153
+ (function (CheckoutToastToneEnum) {
154
+ CheckoutToastToneEnum["SUCCESS"] = "success";
155
+ CheckoutToastToneEnum["WARNING"] = "warning";
156
+ CheckoutToastToneEnum["INFO"] = "info";
157
+ })(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;
@@ -231,6 +231,13 @@ export interface CheckoutCoreActions {
231
231
  simulate: (outcome: SimulatorOutcomeEnum) => void;
232
232
  }
233
233
  /** The checkout's state and actions, the same on the web page and in the payment sheet. */
234
+ /** One short message over the page. */
235
+ export interface CheckoutToast {
236
+ message: string;
237
+ tone: CheckoutToastToneEnum;
238
+ /** The method it is about, shown in place of the tone's icon. */
239
+ logo: ProviderLogoEnum | null;
240
+ }
234
241
  export interface CheckoutCore {
235
242
  /** Null for a link that matches no payment. */
236
243
  view: CheckoutView | null;
@@ -242,7 +249,7 @@ export interface CheckoutCore {
242
249
  /** Seconds before the success screen sends the payer back, null once stopped. */
243
250
  returnLeft: number | null;
244
251
  helpOpen: boolean;
245
- toastMessage: string | null;
252
+ toast: CheckoutToast | null;
246
253
  copied: boolean;
247
254
  /** The Branding tab's preview: nothing can be paid. */
248
255
  preview: boolean;
@@ -49,7 +49,7 @@ export function SheetCodeField({ core }) {
49
49
  };
50
50
  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
51
  // 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: [
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, { 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
53
  styles.dial,
54
54
  { borderColor: colors.line3, backgroundColor: colors.surface },
55
55
  ], 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 })] })] })] }));
@@ -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.
@@ -29,10 +29,10 @@ export function SheetFailed({ core }) {
29
29
  ? t('retry_code')
30
30
  : t('retry'), busy: ui.submitting, onPress: actions.retry }), _jsx(SheetButton, { variant: CheckoutButtonVariantEnum.SECONDARY, 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,4 +1,4 @@
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
3
  import { SheetIconEnum } from "../models/sheet.enum.js";
4
4
  /** A sign in a disc, drawn with views: the sheet ships no icon font or SVG. */
@@ -12,7 +12,8 @@ export function SheetIcon({ kind, color, background, size = 48, }) {
12
12
  sign = _jsx(View, { style: [styles.check, line] });
13
13
  break;
14
14
  case SheetIconEnum.CROSS:
15
- sign = (_jsxs(_Fragment, { children: [_jsx(View, { style: [styles.bar, styles.up, ink] }), _jsx(View, { style: [styles.bar, styles.down, ink] })] }));
15
+ // Crossed in a box of their own length: centred in the disc.
16
+ sign = (_jsxs(View, { style: styles.cross, children: [_jsx(View, { style: [styles.bar, styles.up, ink] }), _jsx(View, { style: [styles.bar, styles.down, ink] })] }));
16
17
  break;
17
18
  case SheetIconEnum.CLOCK:
18
19
  sign = (_jsxs(View, { style: [styles.ring, line], children: [_jsx(View, { style: [styles.hour, ink] }), _jsx(View, { style: [styles.minute, ink] })] }));
@@ -43,6 +44,12 @@ const styles = StyleSheet.create({
43
44
  borderBottomWidth: 3,
44
45
  transform: [{ rotate: '45deg' }],
45
46
  },
47
+ cross: {
48
+ width: 18,
49
+ height: 18,
50
+ alignItems: 'center',
51
+ justifyContent: 'center',
52
+ },
46
53
  bar: { position: 'absolute', width: 18, height: 2.6, borderRadius: 1.3 },
47
54
  up: { transform: [{ rotate: '45deg' }] },
48
55
  down: { transform: [{ rotate: '-45deg' }] },
@@ -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 },
@@ -24,7 +24,7 @@ export function SheetPhoneField({ core, phoneRef }) {
24
24
  },
25
25
  ], 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
26
  // 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 }) }))) })] }));
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, { 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
28
  }
29
29
  const styles = StyleSheet.create({
30
30
  section: { marginTop: 18 },
@@ -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,
@@ -37,3 +37,5 @@ 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;
@@ -76,3 +76,5 @@ 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;
@@ -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 {
@@ -209,7 +211,7 @@ export interface SheetRowProps {
209
211
  urgent?: boolean;
210
212
  }
211
213
  export interface SheetToastProps {
212
- message: string | null;
214
+ toast: CheckoutToast | null;
213
215
  }
214
216
  export interface BoneProps {
215
217
  /** 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.2",
4
4
  "description": "Brissoto Pay for Expo and React Native apps: the payment sheet.",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,