@heyama/pay-react-native 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checkout-core/hooks/use-checkout-controller.js +40 -8
- package/dist/checkout-core/messages/en.d.ts +1 -0
- package/dist/checkout-core/messages/en.js +1 -0
- package/dist/checkout-core/messages/fr.js +1 -0
- package/dist/checkout-core/models/checkout.enum.d.ts +2 -0
- package/dist/checkout-core/models/checkout.enum.js +2 -0
- package/dist/checkout-core/models/checkout.interface.d.ts +7 -0
- package/dist/checkout-core/models/checkout.view.utils.d.ts +2 -0
- package/dist/checkout-core/models/checkout.view.utils.js +12 -0
- package/dist/components/SheetButton.d.ts +1 -1
- package/dist/components/SheetButton.js +8 -4
- package/dist/components/SheetChosenMethod.js +3 -2
- package/dist/components/SheetCodeField.js +3 -2
- package/dist/components/SheetFailed.js +1 -1
- package/dist/components/SheetForm.js +17 -8
- package/dist/components/SheetHeader.js +9 -8
- package/dist/components/SheetIcon.js +2 -1
- package/dist/components/SheetLayout.js +5 -3
- package/dist/components/SheetMethodCard.d.ts +2 -2
- package/dist/components/SheetMethodCard.js +25 -7
- package/dist/components/SheetPhoneField.js +3 -2
- package/dist/components/SheetToast.js +1 -1
- package/dist/hooks/use-shake.d.ts +3 -0
- package/dist/hooks/use-shake.js +18 -0
- package/dist/models/sheet.constants.d.ts +21 -0
- package/dist/models/sheet.constants.js +21 -0
- package/dist/models/sheet.interface.d.ts +6 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ 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, logoOf, screenOf, waitLeftOf, } from "../models/checkout.view.utils.js";
|
|
9
|
+
import { countriesOf, deriveCheckout, initialUi, logoOf, pickedPatch, 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
|
|
@@ -62,6 +63,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
62
63
|
cards: [],
|
|
63
64
|
selected: null,
|
|
64
65
|
canPay: false,
|
|
66
|
+
detected: null,
|
|
65
67
|
mismatch: null,
|
|
66
68
|
suggestion: null,
|
|
67
69
|
};
|
|
@@ -86,6 +88,39 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
86
88
|
update({ dismissed: attemptReference });
|
|
87
89
|
showToast(noAnswer, CheckoutToastToneEnum.WARNING);
|
|
88
90
|
}, [waiting, waitLeft, attemptReference, noAnswer, showToast, update]);
|
|
91
|
+
// A number that tells its method picks it when none is picked: once per
|
|
92
|
+
// number, so Change keeps the list open until the number changes. The app's
|
|
93
|
+
// own number picks without a word. Off the form, or on the Card tab, the
|
|
94
|
+
// number is not looked at: it counts once the payer is back on Mobile Money.
|
|
95
|
+
const detectedMethod = derived.detected;
|
|
96
|
+
const numberE164 = phone.e164;
|
|
97
|
+
const onMobileForm = screen === CheckoutScreenEnum.FORM &&
|
|
98
|
+
ui.tab === CheckoutTabEnum.MOBILE_MONEY;
|
|
99
|
+
const nonePicked = !derived.selected;
|
|
100
|
+
const lastKey = useRef('');
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!onMobileForm)
|
|
103
|
+
return;
|
|
104
|
+
const key = detectedMethod ? `${detectedMethod.id}|${numberE164}` : '';
|
|
105
|
+
if (key === lastKey.current)
|
|
106
|
+
return;
|
|
107
|
+
lastKey.current = key;
|
|
108
|
+
if (!detectedMethod || !nonePicked)
|
|
109
|
+
return;
|
|
110
|
+
update(pickedPatch(detectedMethod.id));
|
|
111
|
+
if (numberE164 !== prefill) {
|
|
112
|
+
showToast(t('toast.switched', { method: detectedMethod.name }), CheckoutToastToneEnum.SUCCESS, logoOf(detectedMethod));
|
|
113
|
+
}
|
|
114
|
+
}, [
|
|
115
|
+
onMobileForm,
|
|
116
|
+
detectedMethod,
|
|
117
|
+
numberE164,
|
|
118
|
+
nonePicked,
|
|
119
|
+
prefill,
|
|
120
|
+
t,
|
|
121
|
+
showToast,
|
|
122
|
+
update,
|
|
123
|
+
]);
|
|
89
124
|
const paymentId = view?.id;
|
|
90
125
|
// A payer opened the link, said once per page (the dashboard's preview has no mutations).
|
|
91
126
|
const opened = useRef(null);
|
|
@@ -191,12 +226,7 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
191
226
|
toast.show(method.note ?? t('toast.unavailable', { method: method.name }), CheckoutToastToneEnum.WARNING, logoOf(method));
|
|
192
227
|
}
|
|
193
228
|
else {
|
|
194
|
-
update(
|
|
195
|
-
method: method.id,
|
|
196
|
-
pickerOpen: false,
|
|
197
|
-
code: '',
|
|
198
|
-
cardComplete: false,
|
|
199
|
-
});
|
|
229
|
+
update(pickedPatch(method.id));
|
|
200
230
|
}
|
|
201
231
|
},
|
|
202
232
|
comingSoon,
|
|
@@ -209,8 +239,10 @@ export function useCheckoutController(view, { clues, mutations, platform }) {
|
|
|
209
239
|
},
|
|
210
240
|
pay: () => {
|
|
211
241
|
const { selected } = derived;
|
|
212
|
-
if (!selected)
|
|
242
|
+
if (!selected) {
|
|
243
|
+
update({ methodMissing: ui.methodMissing + 1 });
|
|
213
244
|
return;
|
|
245
|
+
}
|
|
214
246
|
// The preview offers no card: it shows Mobile Money only.
|
|
215
247
|
if (selected.flow === MethodFlowEnum.CARD_FIELDS) {
|
|
216
248
|
// The fields page pays with its provider, then says how it went.
|
|
@@ -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 {
|
|
@@ -197,6 +202,8 @@ export interface CheckoutDerived {
|
|
|
197
202
|
/** The picked method, while it can still be used. */
|
|
198
203
|
selected: CheckoutMethod | null;
|
|
199
204
|
canPay: boolean;
|
|
205
|
+
/** The available method of the country the typed number belongs to. */
|
|
206
|
+
detected: CheckoutMethod | null;
|
|
200
207
|
/** The number belongs to another method of the country than the one picked. */
|
|
201
208
|
mismatch: CheckoutMethod | null;
|
|
202
209
|
/** After a failure: another method of the same country the payer can use now. */
|
|
@@ -22,6 +22,8 @@ export declare function initialUi(view: CheckoutView, clues: CountryClues): Chec
|
|
|
22
22
|
export declare function screenOf(view: CheckoutView | null | undefined, ui: CheckoutUi): CheckoutScreenEnum;
|
|
23
23
|
/** The brand of a Mobile Money method, which a number's first digits can reveal. */
|
|
24
24
|
export declare function brandOf(method: CheckoutMethod): string;
|
|
25
|
+
/** What the page keeps when a method is picked: the list closes, the rest starts over. */
|
|
26
|
+
export declare function pickedPatch(id: string): Partial<CheckoutUi>;
|
|
25
27
|
/** The tile of a method or an attempt: the generic card for cards, whatever networks they take. */
|
|
26
28
|
export declare function logoOf(of: Pick<CheckoutMethod, 'flow' | 'logos'>): ProviderLogoEnum;
|
|
27
29
|
/** Works out what the screens show from the payment, the page and the number typed. */
|
|
@@ -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;
|
|
@@ -98,6 +99,16 @@ export function screenOf(view, ui) {
|
|
|
98
99
|
export function brandOf(method) {
|
|
99
100
|
return method.logos[0];
|
|
100
101
|
}
|
|
102
|
+
/** What the page keeps when a method is picked: the list closes, the rest starts over. */
|
|
103
|
+
export function pickedPatch(id) {
|
|
104
|
+
return {
|
|
105
|
+
method: id,
|
|
106
|
+
pickerOpen: false,
|
|
107
|
+
code: '',
|
|
108
|
+
cardComplete: false,
|
|
109
|
+
methodMissing: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
101
112
|
/** Everything the picked method needs is in: the card, or the number and its payment code. */
|
|
102
113
|
function readyToPay(method, ui, phone) {
|
|
103
114
|
if (method.flow === MethodFlowEnum.CARD_FIELDS)
|
|
@@ -138,6 +149,7 @@ export function deriveCheckout(view, ui, phone) {
|
|
|
138
149
|
readyToPay(selected, ui, phone) &&
|
|
139
150
|
!ui.submitting &&
|
|
140
151
|
!ui.cardPaying,
|
|
152
|
+
detected,
|
|
141
153
|
// Only a Mobile Money method has a number to belong to another one.
|
|
142
154
|
mismatch: selected && !onCard && detected && detected.id !== selected.id
|
|
143
155
|
? detected
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { SheetButtonProps } from '../models/sheet.interface.js';
|
|
2
|
-
/** The sheet's buttons: filled in the app colour for the main action, outlined for the rest. */
|
|
2
|
+
/** The sheet's buttons: filled in the app colour for the main action, outlined for the rest, plain (no border) for a quiet second choice. */
|
|
3
3
|
export declare function SheetButton({ label, onPress, variant, busy, disabled, inactive, }: SheetButtonProps): import("react").JSX.Element;
|
|
@@ -4,7 +4,8 @@ import { CheckoutButtonVariantEnum } from "../checkout-core/models/checkout.enum
|
|
|
4
4
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
5
5
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
6
6
|
import { SheetText } from "./SheetText.js";
|
|
7
|
-
|
|
7
|
+
import { FIELD_HEIGHT, FIELD_RADIUS } from "../models/sheet.constants.js";
|
|
8
|
+
/** The sheet's buttons: filled in the app colour for the main action, outlined for the rest, plain (no border) for a quiet second choice. */
|
|
8
9
|
export function SheetButton({ label, onPress, variant = CheckoutButtonVariantEnum.PRIMARY, busy = false, disabled = false, inactive = false, }) {
|
|
9
10
|
const { colors, brand } = useSheetTheme();
|
|
10
11
|
const primary = variant === CheckoutButtonVariantEnum.PRIMARY;
|
|
@@ -14,14 +15,17 @@ export function SheetButton({ label, onPress, variant = CheckoutButtonVariantEnu
|
|
|
14
15
|
styles.button,
|
|
15
16
|
primary
|
|
16
17
|
? { backgroundColor: brand }
|
|
17
|
-
:
|
|
18
|
+
: variant !== CheckoutButtonVariantEnum.PLAIN && {
|
|
19
|
+
borderWidth: 1,
|
|
20
|
+
borderColor: colors.line3,
|
|
21
|
+
},
|
|
18
22
|
(disabled || inactive) && styles.disabled,
|
|
19
23
|
], children: [busy && _jsx(ActivityIndicator, { color: ink }), _jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 16, color: ink, style: styles.label, children: label })] }));
|
|
20
24
|
}
|
|
21
25
|
const styles = StyleSheet.create({
|
|
22
26
|
button: {
|
|
23
|
-
minHeight:
|
|
24
|
-
borderRadius:
|
|
27
|
+
minHeight: FIELD_HEIGHT,
|
|
28
|
+
borderRadius: FIELD_RADIUS,
|
|
25
29
|
flexDirection: 'row',
|
|
26
30
|
alignItems: 'center',
|
|
27
31
|
justifyContent: 'center',
|
|
@@ -6,6 +6,7 @@ import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
|
6
6
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
7
7
|
import { SheetLogo } from "./SheetLogo.js";
|
|
8
8
|
import { SheetText } from "./SheetText.js";
|
|
9
|
+
import { FIELD_HEIGHT, FIELD_RADIUS } from "../models/sheet.constants.js";
|
|
9
10
|
/** The method picked, on one row, with Change back to the list: as on the web checkout. */
|
|
10
11
|
export function SheetChosenMethod({ logo, name, sub, onChange, }) {
|
|
11
12
|
const t = useTranslations('checkout');
|
|
@@ -23,10 +24,10 @@ const styles = StyleSheet.create({
|
|
|
23
24
|
flexDirection: 'row',
|
|
24
25
|
alignItems: 'center',
|
|
25
26
|
gap: 12,
|
|
26
|
-
height:
|
|
27
|
+
height: FIELD_HEIGHT,
|
|
27
28
|
marginTop: 12,
|
|
28
29
|
borderWidth: 1,
|
|
29
|
-
borderRadius:
|
|
30
|
+
borderRadius: FIELD_RADIUS,
|
|
30
31
|
paddingLeft: 8,
|
|
31
32
|
paddingRight: 6,
|
|
32
33
|
},
|
|
@@ -7,6 +7,7 @@ import { telHref } from "../checkout-core/models/checkout.phone.utils.js";
|
|
|
7
7
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
8
8
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
9
9
|
import { SheetInput, SheetText } from "./SheetText.js";
|
|
10
|
+
import { FIELD_HEIGHT, FIELD_RADIUS } from "../models/sheet.constants.js";
|
|
10
11
|
/**
|
|
11
12
|
* OTP methods: Dial opens the network's code in the phone dialler for a
|
|
12
13
|
* one-time payment code (on Android; on iOS it says to dial it by hand);
|
|
@@ -58,9 +59,9 @@ const styles = StyleSheet.create({
|
|
|
58
59
|
section: { marginTop: 10 },
|
|
59
60
|
label: { marginBottom: 8 },
|
|
60
61
|
input: {
|
|
61
|
-
height:
|
|
62
|
+
height: FIELD_HEIGHT,
|
|
62
63
|
borderWidth: 1,
|
|
63
|
-
borderRadius:
|
|
64
|
+
borderRadius: FIELD_RADIUS,
|
|
64
65
|
paddingHorizontal: 14,
|
|
65
66
|
fontSize: 18,
|
|
66
67
|
letterSpacing: 5,
|
|
@@ -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.
|
|
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
|
-
|
|
16
|
-
|
|
16
|
+
import { FIELD_HEIGHT, FIELD_RADIUS, TABS_INSET, } from "../models/sheet.constants.js";
|
|
17
|
+
/** Two cards a row, the last one alone when they are odd; red with a hint, and shaken, after Pay with none picked. */
|
|
18
|
+
function MethodGrid({ cards, missing }) {
|
|
19
|
+
const t = useTranslations('checkout');
|
|
20
|
+
const { colors } = useSheetTheme();
|
|
21
|
+
const shake = useShake(missing);
|
|
17
22
|
const rows = [];
|
|
18
23
|
for (let index = 0; index < cards.length; index += 2) {
|
|
19
24
|
rows.push(cards.slice(index, index + 2));
|
|
20
25
|
}
|
|
21
|
-
return (
|
|
26
|
+
return (_jsxs(Animated.View, { testID: "method-grid", accessibilityRole: "radiogroup", style: [styles.grid, { transform: [{ translateX: shake }] }], children: [rows.map((row) => (_jsxs(View, { style: styles.gridRow, children: [row.map(({ id, ...card }) => (_jsx(SheetMethodCard, { ...card, missing: missing > 0, alone: cards.length === 1 }, id))), row.length === 1 && cards.length > 1 && (_jsx(View, { style: styles.gridSpacer }))] }, row[0].id))), missing > 0 && (_jsx(SheetText, { accessibilityRole: "alert", weight: FigtreeEnum.MEDIUM, size: 13, color: colors.danger, children: t('method_missing') }))] }));
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* The form of design A: the country when there are several, the Mobile
|
|
@@ -61,7 +66,7 @@ export function SheetForm({ core, phoneRef, cardFields }) {
|
|
|
61
66
|
? { backgroundColor: colors.ink, borderColor: colors.ink }
|
|
62
67
|
: { borderColor: colors.line3 },
|
|
63
68
|
], children: [_jsx(SheetText, { size: 14, children: flagOf(country) }), _jsx(SheetText, { weight: FigtreeEnum.SEMI_BOLD, size: 13, color: on ? colors.surface : colors.ink, children: t(`countries.${country}`) })] }, country));
|
|
64
|
-
}) })), _jsxs(View, { accessibilityRole: "tablist", style: [styles.tabs, { backgroundColor: colors.fill }], children: [tab(CheckoutTabEnum.MOBILE_MONEY, t('tab_wallet')), tab(CheckoutTabEnum.CARD, t('card'))] }), derived.selected ? (_jsx(SheetChosenMethod, { logo: logoOf(derived.selected), name: derived.selected.name, sub: derived.selected.network, onChange: actions.openPicker })) : (_jsx(MethodGrid, { cards: cards })), !onCard && _jsx(SheetPhoneField, { core: core, phoneRef: phoneRef }), !onCard && derived.selected?.flow === MethodFlowEnum.OTP && (_jsx(SheetCodeField, { core: core })), onCard && derived.selected?.flow === MethodFlowEnum.CARD_FIELDS && (_jsx(SheetCardFields, { core: core, bridge: cardFields }))] }));
|
|
69
|
+
}) })), _jsxs(View, { testID: "sheet-tabs", accessibilityRole: "tablist", style: [styles.tabs, { backgroundColor: colors.fill }], children: [tab(CheckoutTabEnum.MOBILE_MONEY, t('tab_wallet')), tab(CheckoutTabEnum.CARD, t('card'))] }), derived.selected ? (_jsx(SheetChosenMethod, { logo: logoOf(derived.selected), name: derived.selected.name, sub: derived.selected.network, onChange: actions.openPicker })) : (_jsx(MethodGrid, { cards: cards, missing: ui.methodMissing })), !onCard && _jsx(SheetPhoneField, { core: core, phoneRef: phoneRef }), !onCard && derived.selected?.flow === MethodFlowEnum.OTP && (_jsx(SheetCodeField, { core: core })), onCard && derived.selected?.flow === MethodFlowEnum.CARD_FIELDS && (_jsx(SheetCardFields, { core: core, bridge: cardFields }))] }));
|
|
65
70
|
}
|
|
66
71
|
const styles = StyleSheet.create({
|
|
67
72
|
form: { paddingHorizontal: 18, paddingTop: 14, paddingBottom: 8 },
|
|
@@ -80,13 +85,17 @@ const styles = StyleSheet.create({
|
|
|
80
85
|
paddingHorizontal: 12,
|
|
81
86
|
paddingVertical: 6,
|
|
82
87
|
},
|
|
83
|
-
tabs: {
|
|
88
|
+
tabs: {
|
|
89
|
+
flexDirection: 'row',
|
|
90
|
+
height: FIELD_HEIGHT,
|
|
91
|
+
borderRadius: FIELD_RADIUS,
|
|
92
|
+
padding: TABS_INSET,
|
|
93
|
+
},
|
|
84
94
|
tab: {
|
|
85
95
|
flex: 1,
|
|
86
96
|
alignItems: 'center',
|
|
87
97
|
justifyContent: 'center',
|
|
88
|
-
|
|
89
|
-
borderRadius: 9,
|
|
98
|
+
borderRadius: FIELD_RADIUS - TABS_INSET,
|
|
90
99
|
},
|
|
91
100
|
tabOn: {
|
|
92
101
|
shadowColor: 'black',
|
|
@@ -3,6 +3,7 @@ import { Image, Pressable, StyleSheet, View } from 'react-native';
|
|
|
3
3
|
import { useTranslations } from 'use-intl';
|
|
4
4
|
import { MEDIA_LINE } from "../checkout-core/models/checkout.constants.js";
|
|
5
5
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
6
|
+
import { SHEET_BADGE_RADIUS, SHEET_BADGE_SIZE, } from "../models/sheet.constants.js";
|
|
6
7
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
7
8
|
import { SheetBone } from "./SheetBone.js";
|
|
8
9
|
import { SheetText } from "./SheetText.js";
|
|
@@ -14,8 +15,8 @@ export function SheetHeader({ view, loading, onClose, dragHandlers, }) {
|
|
|
14
15
|
const t = useTranslations('checkout');
|
|
15
16
|
const { colors, brand } = useSheetTheme();
|
|
16
17
|
const app = view?.app;
|
|
17
|
-
return (_jsxs(View, { ...dragHandlers, children: [_jsx(View, { style: [styles.grabber, { backgroundColor: colors.line3 }] }), _jsxs(View, { style: styles.row, children: [loading ? (_jsxs(_Fragment, { children: [_jsx(SheetBone, { width:
|
|
18
|
-
(app.logoUrl ? (_jsx(Image, { source: { uri: app.logoUrl }, accessibilityIgnoresInvertColors: true, style: styles.logo })) : (_jsx(View, { testID: "app-initial", style: [styles.initial, { backgroundColor: brand }], children: _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size:
|
|
18
|
+
return (_jsxs(View, { ...dragHandlers, children: [_jsx(View, { style: [styles.grabber, { backgroundColor: colors.line3 }] }), _jsxs(View, { style: styles.row, children: [loading ? (_jsxs(_Fragment, { children: [_jsx(SheetBone, { width: SHEET_BADGE_SIZE, height: SHEET_BADGE_SIZE, radius: SHEET_BADGE_RADIUS }), _jsx(View, { style: styles.name, children: _jsx(SheetBone, { width: 84, height: 14 }) })] })) : (app &&
|
|
19
|
+
(app.logoUrl ? (_jsx(Image, { testID: "app-logo", source: { uri: app.logoUrl }, accessibilityIgnoresInvertColors: true, style: styles.logo })) : (_jsx(View, { testID: "app-initial", style: [styles.initial, { backgroundColor: brand }], children: _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 18, color: "white", style: styles.centred, children: app.name.charAt(0) }) })))), !loading && (_jsx(SheetText, { weight: FigtreeEnum.BOLD, size: 16, numberOfLines: 1, accessibilityRole: "header", style: [styles.name, styles.centred], children: app?.name ?? 'Brissoto Pay' })), !loading && view?.test && (_jsx(View, { style: [styles.badge, { backgroundColor: colors.warnSoft }], children: _jsx(SheetText, { weight: FigtreeEnum.EXTRA_BOLD, size: 10.5, color: colors.warnInk, style: [styles.badgeText, styles.centred], children: t('test_badge') }) })), _jsxs(Pressable, { onPress: onClose, accessibilityRole: "button", accessibilityLabel: t('close'), hitSlop: 8, style: [styles.close, { backgroundColor: colors.fill }], children: [_jsx(View, { style: [styles.bar, styles.barUp, { backgroundColor: colors.ink2 }] }), _jsx(View, { style: [
|
|
19
20
|
styles.bar,
|
|
20
21
|
styles.barDown,
|
|
21
22
|
{ backgroundColor: colors.ink2 },
|
|
@@ -37,16 +38,16 @@ const styles = StyleSheet.create({
|
|
|
37
38
|
paddingTop: 10,
|
|
38
39
|
},
|
|
39
40
|
logo: {
|
|
40
|
-
width:
|
|
41
|
-
height:
|
|
42
|
-
borderRadius:
|
|
41
|
+
width: SHEET_BADGE_SIZE,
|
|
42
|
+
height: SHEET_BADGE_SIZE,
|
|
43
|
+
borderRadius: SHEET_BADGE_RADIUS,
|
|
43
44
|
borderWidth: 1,
|
|
44
45
|
borderColor: MEDIA_LINE,
|
|
45
46
|
},
|
|
46
47
|
initial: {
|
|
47
|
-
width:
|
|
48
|
-
height:
|
|
49
|
-
borderRadius:
|
|
48
|
+
width: SHEET_BADGE_SIZE,
|
|
49
|
+
height: SHEET_BADGE_SIZE,
|
|
50
|
+
borderRadius: SHEET_BADGE_RADIUS,
|
|
50
51
|
alignItems: 'center',
|
|
51
52
|
justifyContent: 'center',
|
|
52
53
|
},
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { SHEET_BADGE_SIZE } from "../models/sheet.constants.js";
|
|
3
4
|
import { SheetIconEnum } from "../models/sheet.enum.js";
|
|
4
5
|
/** A sign in a disc, drawn with views: the sheet ships no icon font or SVG. */
|
|
5
|
-
export function SheetIcon({ kind, color, background, size =
|
|
6
|
+
export function SheetIcon({ kind, color, background, size = SHEET_BADGE_SIZE, }) {
|
|
6
7
|
const scale = size / 48;
|
|
7
8
|
const ink = { backgroundColor: color };
|
|
8
9
|
const line = { borderColor: color };
|
|
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
3
3
|
import { Keyboard, Platform, ScrollView, StyleSheet, View } from 'react-native';
|
|
4
4
|
import { useTranslations } from 'use-intl';
|
|
5
5
|
import { useSheetTheme } from "../hooks/use-sheet-theme.js";
|
|
6
|
+
import { SHEET_FOOT_GAP, SHEET_FOOT_TOP } from "../models/sheet.constants.js";
|
|
6
7
|
import { FigtreeEnum } from "../models/sheet.enum.js";
|
|
7
8
|
import { SheetText } from "./SheetText.js";
|
|
8
9
|
/**
|
|
@@ -39,10 +40,11 @@ const styles = StyleSheet.create({
|
|
|
39
40
|
foot: {
|
|
40
41
|
borderTopWidth: 1,
|
|
41
42
|
paddingHorizontal: 18,
|
|
42
|
-
paddingTop:
|
|
43
|
+
paddingTop: SHEET_FOOT_TOP,
|
|
43
44
|
paddingBottom: 28,
|
|
44
|
-
gap:
|
|
45
|
+
gap: SHEET_FOOT_GAP,
|
|
45
46
|
},
|
|
46
47
|
footOnKeyboard: { paddingBottom: 12 },
|
|
47
|
-
|
|
48
|
+
// Android's extra font padding would push it off the middle.
|
|
49
|
+
secure: { textAlign: 'center', includeFontPadding: false },
|
|
48
50
|
});
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { MethodCardProps } from '../models/sheet.interface.js';
|
|
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;
|
|
2
|
+
/** One method of the grid: logo and radio dot, then its name and network, or why it is greyed. Alone, one full-width row. Picked, it becomes the chosen row. */
|
|
3
|
+
export declare function SheetMethodCard({ logo, name, sub, muted, missing, alone, onSelect, }: MethodCardProps): import("react").JSX.Element;
|
|
@@ -1,32 +1,50 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
|
3
3
|
import { LOGO_SIZE } from "../checkout-core/models/checkout.constants.js";
|
|
4
4
|
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
|
-
|
|
9
|
-
|
|
8
|
+
import { FIELD_RADIUS } from "../models/sheet.constants.js";
|
|
9
|
+
/** One method of the grid: logo and radio dot, then its name and network, or why it is greyed. Alone, one full-width row. Picked, it becomes the chosen row. */
|
|
10
|
+
export function SheetMethodCard({ logo, name, sub, muted, missing, alone, onSelect, }) {
|
|
10
11
|
const { colors } = useSheetTheme();
|
|
11
|
-
|
|
12
|
+
const red = missing && !muted;
|
|
13
|
+
const texts = (_jsxs(_Fragment, { children: [_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 })] }));
|
|
14
|
+
return (_jsxs(TouchableOpacity, { onPress: onSelect, disabled: muted, activeOpacity: 0.7, accessibilityRole: "radio", accessibilityLabel: `${name}, ${sub}`, accessibilityState: { checked: false, disabled: muted }, style: [
|
|
15
|
+
styles.card,
|
|
16
|
+
red
|
|
17
|
+
? [styles.cardMissing, { borderColor: colors.danger }]
|
|
18
|
+
: { borderColor: colors.line2 },
|
|
19
|
+
], children: [_jsxs(View, { style: alone ? styles.row : styles.top, children: [_jsx(SheetLogo, { logo: logo, size: LOGO_SIZE.ROW, muted: muted }), alone && _jsx(View, { style: styles.text, children: texts }), _jsx(View, { style: [
|
|
12
20
|
styles.radio,
|
|
13
|
-
{
|
|
14
|
-
|
|
21
|
+
{
|
|
22
|
+
borderColor: muted
|
|
23
|
+
? colors.line2
|
|
24
|
+
: red
|
|
25
|
+
? colors.danger
|
|
26
|
+
: colors.line4,
|
|
27
|
+
},
|
|
28
|
+
] })] }), !alone && texts] }));
|
|
15
29
|
}
|
|
16
30
|
const styles = StyleSheet.create({
|
|
17
31
|
card: {
|
|
18
32
|
flex: 1,
|
|
19
33
|
minWidth: 0,
|
|
20
34
|
borderWidth: 1,
|
|
21
|
-
borderRadius:
|
|
35
|
+
borderRadius: FIELD_RADIUS,
|
|
22
36
|
padding: 12,
|
|
23
37
|
gap: 2,
|
|
24
38
|
},
|
|
39
|
+
// 1.5 like the web's ring; the padding gives back the half point.
|
|
40
|
+
cardMissing: { borderWidth: 1.5, padding: 11.5 },
|
|
25
41
|
top: {
|
|
26
42
|
flexDirection: 'row',
|
|
27
43
|
alignItems: 'flex-start',
|
|
28
44
|
justifyContent: 'space-between',
|
|
29
45
|
marginBottom: 10,
|
|
30
46
|
},
|
|
47
|
+
row: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
|
48
|
+
text: { flex: 1, minWidth: 0 },
|
|
31
49
|
radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 1.5 },
|
|
32
50
|
});
|
|
@@ -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:
|
|
36
|
+
height: FIELD_HEIGHT,
|
|
36
37
|
borderWidth: 1,
|
|
37
|
-
borderRadius:
|
|
38
|
+
borderRadius: FIELD_RADIUS,
|
|
38
39
|
overflow: 'hidden',
|
|
39
40
|
},
|
|
40
41
|
dial: {
|
|
@@ -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,10 @@ 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;
|
|
184
|
+
/** The only method of the tab: a full-width row. */
|
|
185
|
+
alone?: boolean;
|
|
182
186
|
onSelect?: () => void;
|
|
183
187
|
}
|
|
184
188
|
export interface ChosenMethodProps {
|
|
@@ -194,6 +198,8 @@ export interface MethodGridCard extends MethodCardProps {
|
|
|
194
198
|
}
|
|
195
199
|
export interface MethodGridProps {
|
|
196
200
|
cards: Array<MethodGridCard>;
|
|
201
|
+
/** Pay presses with no method picked: red, and a shake per press. */
|
|
202
|
+
missing: number;
|
|
197
203
|
}
|
|
198
204
|
export interface SheetIconProps {
|
|
199
205
|
kind: SheetIconEnum;
|