@heyama/pay-react-native 1.0.2 → 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.
@@ -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
@@ -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
  },
@@ -209,8 +211,10 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
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.
@@ -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
  }
@@ -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 = {}));
@@ -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 {
@@ -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);
@@ -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,
@@ -27,7 +27,7 @@ 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
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,
@@ -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
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 };
@@ -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
  });
@@ -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');
@@ -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: {
@@ -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
+ }
@@ -39,3 +39,24 @@ export declare const SHEET_FLICK_SPEED = 0.8;
39
39
  export declare const SHEET_RESIZE: LayoutAnimationConfig;
40
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
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;
@@ -78,3 +78,24 @@ export const SHEET_RESIZE = {
78
78
  };
79
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
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;
@@ -179,6 +179,8 @@ export interface MethodCardProps {
179
179
  sub: string;
180
180
  /** Greyed and not pressable: in maintenance, or not offered yet. */
181
181
  muted: boolean;
182
+ /** Pay was pressed with no method picked: the card is ringed in red. */
183
+ missing?: boolean;
182
184
  onSelect?: () => void;
183
185
  }
184
186
  export interface ChosenMethodProps {
@@ -194,6 +196,8 @@ export interface MethodGridCard extends MethodCardProps {
194
196
  }
195
197
  export interface MethodGridProps {
196
198
  cards: Array<MethodGridCard>;
199
+ /** Pay presses with no method picked: red, and a shake per press. */
200
+ missing: number;
197
201
  }
198
202
  export interface SheetIconProps {
199
203
  kind: SheetIconEnum;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyama/pay-react-native",
3
- "version": "1.0.2",
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,