@heyama/pay-react-native 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +99 -2
- package/assets/fonts/Figtree-Bold.ttf +0 -0
- package/assets/fonts/Figtree-ExtraBold.ttf +0 -0
- package/assets/fonts/Figtree-Medium.ttf +0 -0
- package/assets/fonts/Figtree-Regular.ttf +0 -0
- package/assets/fonts/Figtree-SemiBold.ttf +0 -0
- package/assets/fonts/OFL.txt +93 -0
- package/assets/logos/apple-pay.png +0 -0
- package/assets/logos/card.png +0 -0
- package/assets/logos/google-pay.png +0 -0
- package/assets/logos/mastercard.png +0 -0
- package/assets/logos/mixx.png +0 -0
- package/assets/logos/moov.png +0 -0
- package/assets/logos/mtn.png +0 -0
- package/assets/logos/orange.png +0 -0
- package/assets/logos/paypal.png +0 -0
- package/assets/logos/visa.png +0 -0
- package/assets/logos/wave.png +0 -0
- package/dist/checkout-core/hooks/use-checkout-controller.d.ts +9 -0
- package/dist/checkout-core/hooks/use-checkout-controller.js +319 -0
- package/dist/checkout-core/hooks/use-checkout-timers.d.ts +15 -0
- package/dist/checkout-core/hooks/use-checkout-timers.js +44 -0
- package/dist/checkout-core/hooks/use-phone-number.d.ts +7 -0
- package/dist/checkout-core/hooks/use-phone-number.js +82 -0
- package/dist/checkout-core/messages/en.d.ts +252 -0
- package/dist/checkout-core/messages/en.js +242 -0
- package/dist/checkout-core/messages/fr.d.ts +4 -0
- package/dist/checkout-core/messages/fr.js +244 -0
- package/dist/checkout-core/models/checkout.card.utils.d.ts +22 -0
- package/dist/checkout-core/models/checkout.card.utils.js +101 -0
- package/dist/checkout-core/models/checkout.constants.d.ts +56 -0
- package/dist/checkout-core/models/checkout.constants.js +142 -0
- package/dist/checkout-core/models/checkout.enum.d.ts +131 -0
- package/dist/checkout-core/models/checkout.enum.js +148 -0
- package/dist/checkout-core/models/checkout.interface.d.ts +417 -0
- package/dist/checkout-core/models/checkout.interface.js +1 -0
- package/dist/checkout-core/models/checkout.money.utils.d.ts +7 -0
- package/dist/checkout-core/models/checkout.money.utils.js +21 -0
- package/dist/checkout-core/models/checkout.phone.utils.d.ts +33 -0
- package/dist/checkout-core/models/checkout.phone.utils.js +159 -0
- package/dist/checkout-core/models/checkout.view.utils.d.ts +34 -0
- package/dist/checkout-core/models/checkout.view.utils.js +205 -0
- package/dist/checkout-core/models/domain.enum.d.ts +56 -0
- package/dist/checkout-core/models/domain.enum.js +62 -0
- package/dist/components/BrissotoPayProvider.d.ts +6 -0
- package/dist/components/BrissotoPayProvider.js +41 -0
- package/dist/components/PaymentSheetModal.d.ts +8 -0
- package/dist/components/PaymentSheetModal.js +72 -0
- package/dist/components/SheetBone.d.ts +3 -0
- package/dist/components/SheetBone.js +12 -0
- package/dist/components/SheetButton.d.ts +3 -0
- package/dist/components/SheetButton.js +33 -0
- package/dist/components/SheetCardFields.d.ts +8 -0
- package/dist/components/SheetCardFields.js +66 -0
- package/dist/components/SheetCodeField.d.ts +8 -0
- package/dist/components/SheetCodeField.js +95 -0
- package/dist/components/SheetContent.d.ts +3 -0
- package/dist/components/SheetContent.js +106 -0
- package/dist/components/SheetFailed.d.ts +3 -0
- package/dist/components/SheetFailed.js +43 -0
- package/dist/components/SheetForm.d.ts +7 -0
- package/dist/components/SheetForm.js +100 -0
- package/dist/components/SheetHeader.d.ts +6 -0
- package/dist/components/SheetHeader.js +69 -0
- package/dist/components/SheetIcon.d.ts +3 -0
- package/dist/components/SheetIcon.js +70 -0
- package/dist/components/SheetLayout.d.ts +8 -0
- package/dist/components/SheetLayout.js +35 -0
- package/dist/components/SheetLogo.d.ts +3 -0
- package/dist/components/SheetLogo.js +20 -0
- package/dist/components/SheetMessage.d.ts +3 -0
- package/dist/components/SheetMessage.js +15 -0
- package/dist/components/SheetMethodCard.d.ts +3 -0
- package/dist/components/SheetMethodCard.js +39 -0
- package/dist/components/SheetPhoneField.d.ts +3 -0
- package/dist/components/SheetPhoneField.js +56 -0
- package/dist/components/SheetRow.d.ts +3 -0
- package/dist/components/SheetRow.js +28 -0
- package/dist/components/SheetSimulator.d.ts +6 -0
- package/dist/components/SheetSimulator.js +39 -0
- package/dist/components/SheetSkeleton.d.ts +5 -0
- package/dist/components/SheetSkeleton.js +23 -0
- package/dist/components/SheetSuccess.d.ts +3 -0
- package/dist/components/SheetSuccess.js +51 -0
- package/dist/components/SheetSummary.d.ts +3 -0
- package/dist/components/SheetSummary.js +32 -0
- package/dist/components/SheetTerminal.d.ts +3 -0
- package/dist/components/SheetTerminal.js +21 -0
- package/dist/components/SheetText.d.ts +5 -0
- package/dist/components/SheetText.js +25 -0
- package/dist/components/SheetToast.d.ts +3 -0
- package/dist/components/SheetToast.js +24 -0
- package/dist/components/SheetWaiting.d.ts +7 -0
- package/dist/components/SheetWaiting.js +77 -0
- package/dist/hooks/use-native-platform.d.ts +10 -0
- package/dist/hooks/use-native-platform.js +47 -0
- package/dist/hooks/use-payment-sheet.d.ts +4 -0
- package/dist/hooks/use-payment-sheet.js +10 -0
- package/dist/hooks/use-sheet-motion.d.ts +7 -0
- package/dist/hooks/use-sheet-motion.js +62 -0
- package/dist/hooks/use-sheet-resize.d.ts +6 -0
- package/dist/hooks/use-sheet-resize.js +15 -0
- package/dist/hooks/use-sheet-theme.d.ts +4 -0
- package/dist/hooks/use-sheet-theme.js +12 -0
- package/dist/hooks/use-watched-payment.d.ts +6 -0
- package/dist/hooks/use-watched-payment.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/models/sheet.constants.d.ts +39 -0
- package/dist/models/sheet.constants.js +78 -0
- package/dist/models/sheet.enum.d.ts +16 -0
- package/dist/models/sheet.enum.js +18 -0
- package/dist/models/sheet.interface.d.ts +225 -0
- package/dist/models/sheet.interface.js +1 -0
- package/dist/models/sheet.utils.d.ts +43 -0
- package/dist/models/sheet.utils.js +132 -0
- package/dist/services/checkout.service.d.ts +7 -0
- package/dist/services/checkout.service.js +39 -0
- package/package.json +58 -4
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CurrencyEnum } from './domain.enum.js';
|
|
2
|
+
/** 15000 XAF → "15 000", 1250 EUR → "12.50": grouped with a narrow no-break space, so it never wraps. */
|
|
3
|
+
export declare function formatAmount(minor: number, currency: CurrencyEnum): string;
|
|
4
|
+
/** "15 000 FCFA". */
|
|
5
|
+
export declare function formatMoney(minor: number, currency: CurrencyEnum): string;
|
|
6
|
+
/** "02:05" style clock for the approval countdown. */
|
|
7
|
+
export declare function formatClock(totalSeconds: number): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CURRENCY_DECIMALS, CURRENCY_LABEL } from "./checkout.constants.js";
|
|
2
|
+
const THIN_SPACE = ' ';
|
|
3
|
+
/** 15000 XAF → "15 000", 1250 EUR → "12.50": grouped with a narrow no-break space, so it never wraps. */
|
|
4
|
+
export function formatAmount(minor, currency) {
|
|
5
|
+
const decimals = CURRENCY_DECIMALS[currency];
|
|
6
|
+
const [whole, fraction] = (minor / 10 ** decimals)
|
|
7
|
+
.toFixed(decimals)
|
|
8
|
+
.split('.');
|
|
9
|
+
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, THIN_SPACE);
|
|
10
|
+
return fraction ? `${grouped}.${fraction}` : grouped;
|
|
11
|
+
}
|
|
12
|
+
/** "15 000 FCFA". */
|
|
13
|
+
export function formatMoney(minor, currency) {
|
|
14
|
+
return `${formatAmount(minor, currency)} ${CURRENCY_LABEL[currency]}`;
|
|
15
|
+
}
|
|
16
|
+
/** "02:05" style clock for the approval countdown. */
|
|
17
|
+
export function formatClock(totalSeconds) {
|
|
18
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
19
|
+
const seconds = String(totalSeconds % 60).padStart(2, '0');
|
|
20
|
+
return `${minutes}:${seconds}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CountryEnum } from './domain.enum.js';
|
|
2
|
+
import type { OperatorDetection, PhoneInputs, PhoneStatus, TypedNumber } from './checkout.interface.js';
|
|
3
|
+
/** Which operator a number belongs to, from its first digits. */
|
|
4
|
+
export declare function detectOperator(country: CountryEnum, digits: string): OperatorDetection;
|
|
5
|
+
/**
|
|
6
|
+
* What the field should say about the number typed so far, by the country's
|
|
7
|
+
* own mobile rules. Errors that typing more can never fix (a wrong first
|
|
8
|
+
* digit) show at once; the rest wait until the field is left or Pay pressed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getPhoneStatus({ country, digits, foreign, touched, focused, submitTried, }: PhoneInputs): PhoneStatus;
|
|
11
|
+
/** "691234567" → "6 91 23 45 67", grouped the way the country writes it. */
|
|
12
|
+
export declare function formatNational(digits: string, country: CountryEnum): string;
|
|
13
|
+
/** "+33612345678" → "+33 6 12 34 56 78": a full number, grouped its country's way. */
|
|
14
|
+
export declare function formatInternational(number: string): string;
|
|
15
|
+
/** A real-looking mobile number of the country, for the empty field. */
|
|
16
|
+
export declare function placeholderOf(country: CountryEnum): string;
|
|
17
|
+
/** The number as providers want it: `+237691234567`. Empty while incomplete. */
|
|
18
|
+
export declare function toE164(digits: string, country: CountryEnum): string;
|
|
19
|
+
/**
|
|
20
|
+
* Reads what the payer typed or pasted. A national number stays in the
|
|
21
|
+
* country; a full international one (`+225…`, `00225…`) moves to its country
|
|
22
|
+
* when the payment can be paid there, and is kept as typed until its country
|
|
23
|
+
* is known.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readTypedNumber(raw: string, country: CountryEnum, countries: Array<CountryEnum>): TypedNumber;
|
|
26
|
+
/** "6 91 23 45 67" → "6 91 •• •• 67": keeps the first 3 and last 2 digits. */
|
|
27
|
+
export declare function maskNumber(formatted: string): string;
|
|
28
|
+
/** Where the caret goes in `formatted` to sit after its `count`-th digit. */
|
|
29
|
+
export declare function caretAfterDigits(formatted: string, count: number): number;
|
|
30
|
+
/** A USSD code as a link that dials it: `#` must be escaped in a `tel:` URL. */
|
|
31
|
+
export declare function telHref(code: string): string;
|
|
32
|
+
/** `+237691234567` → `+237 6 91 •• •• 67`: the dial code, then the number masked. */
|
|
33
|
+
export declare function maskE164(phone: string): string;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { AsYouType, getExampleNumber, isValidPhoneNumber, parsePhoneNumberFromString, validatePhoneNumberLength, } from 'libphonenumber-js/mobile';
|
|
2
|
+
import examples from 'libphonenumber-js/mobile/examples';
|
|
3
|
+
import { DIAL_CODES } from "./checkout.constants.js";
|
|
4
|
+
import { CountryEnum } from "./domain.enum.js";
|
|
5
|
+
import { MethodBrandEnum, PhoneErrorEnum } from "./checkout.enum.js";
|
|
6
|
+
/** Cameroon mobile numbers are 9 digits and always start with 6. */
|
|
7
|
+
const CAMEROON_LENGTH = 9;
|
|
8
|
+
const MTN_CAMEROON = /^6(7\d|5[0-4]|8[0-4])$/;
|
|
9
|
+
const ORANGE_CAMEROON = /^6(9\d|5[5-9]|8[5-9]|4\d)$/;
|
|
10
|
+
const CI_OPERATOR_BY_PREFIX = {
|
|
11
|
+
'07': MethodBrandEnum.ORANGE,
|
|
12
|
+
'05': MethodBrandEnum.MTN,
|
|
13
|
+
'01': MethodBrandEnum.MOOV,
|
|
14
|
+
};
|
|
15
|
+
/** `cm` → `CM`, the library's country code. */
|
|
16
|
+
function codeOf(country) {
|
|
17
|
+
return country.toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
/** Which operator a number belongs to, from its first digits. */
|
|
20
|
+
export function detectOperator(country, digits) {
|
|
21
|
+
if (country === CountryEnum.CAMEROON) {
|
|
22
|
+
if (digits.length < 3)
|
|
23
|
+
return { operator: null, unrecognized: false };
|
|
24
|
+
const prefix = digits.slice(0, 3);
|
|
25
|
+
if (MTN_CAMEROON.test(prefix))
|
|
26
|
+
return { operator: MethodBrandEnum.MTN, unrecognized: false };
|
|
27
|
+
if (ORANGE_CAMEROON.test(prefix))
|
|
28
|
+
return { operator: MethodBrandEnum.ORANGE, unrecognized: false };
|
|
29
|
+
return { operator: null, unrecognized: true };
|
|
30
|
+
}
|
|
31
|
+
if (country === CountryEnum.COTE_D_IVOIRE && digits.length >= 2) {
|
|
32
|
+
return {
|
|
33
|
+
operator: CI_OPERATOR_BY_PREFIX[digits.slice(0, 2)] ?? null,
|
|
34
|
+
unrecognized: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return { operator: null, unrecognized: false };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* What the field should say about the number typed so far, by the country's
|
|
41
|
+
* own mobile rules. Errors that typing more can never fix (a wrong first
|
|
42
|
+
* digit) show at once; the rest wait until the field is left or Pay pressed.
|
|
43
|
+
*/
|
|
44
|
+
export function getPhoneStatus({ country, digits, foreign, touched, focused, submitTried, }) {
|
|
45
|
+
const isCameroon = country === CountryEnum.CAMEROON;
|
|
46
|
+
const { operator, unrecognized } = detectOperator(country, digits);
|
|
47
|
+
const valid = isValidPhoneNumber(digits, codeOf(country));
|
|
48
|
+
let error = null;
|
|
49
|
+
if (isCameroon && digits.length > 0 && !digits.startsWith('6')) {
|
|
50
|
+
error = PhoneErrorEnum.START;
|
|
51
|
+
}
|
|
52
|
+
else if (isCameroon && digits.length === CAMEROON_LENGTH && unrecognized) {
|
|
53
|
+
error = PhoneErrorEnum.OPERATOR;
|
|
54
|
+
}
|
|
55
|
+
else if ((touched && !focused) || submitTried) {
|
|
56
|
+
if (foreign) {
|
|
57
|
+
error = PhoneErrorEnum.COUNTRY;
|
|
58
|
+
}
|
|
59
|
+
else if (digits.length === 0) {
|
|
60
|
+
if (submitTried)
|
|
61
|
+
error = PhoneErrorEnum.EMPTY;
|
|
62
|
+
}
|
|
63
|
+
else if (validatePhoneNumberLength(digits, codeOf(country)) === 'TOO_SHORT') {
|
|
64
|
+
error = PhoneErrorEnum.SHORT;
|
|
65
|
+
}
|
|
66
|
+
else if (!valid) {
|
|
67
|
+
error = PhoneErrorEnum.INVALID;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { error, valid: error === null && valid, operator };
|
|
71
|
+
}
|
|
72
|
+
/** "691234567" → "6 91 23 45 67", grouped the way the country writes it. */
|
|
73
|
+
export function formatNational(digits, country) {
|
|
74
|
+
return new AsYouType(codeOf(country)).input(digits);
|
|
75
|
+
}
|
|
76
|
+
/** "+33612345678" → "+33 6 12 34 56 78": a full number, grouped its country's way. */
|
|
77
|
+
export function formatInternational(number) {
|
|
78
|
+
return new AsYouType().input(number);
|
|
79
|
+
}
|
|
80
|
+
/** A real-looking mobile number of the country, for the empty field. */
|
|
81
|
+
export function placeholderOf(country) {
|
|
82
|
+
return getExampleNumber(codeOf(country), examples).formatNational();
|
|
83
|
+
}
|
|
84
|
+
/** The number as providers want it: `+237691234567`. Empty while incomplete. */
|
|
85
|
+
export function toE164(digits, country) {
|
|
86
|
+
return parsePhoneNumberFromString(digits, codeOf(country))?.number ?? '';
|
|
87
|
+
}
|
|
88
|
+
/** Drops the digits past the longest number the country has. */
|
|
89
|
+
function fit(digits, country) {
|
|
90
|
+
let kept = digits;
|
|
91
|
+
while (validatePhoneNumberLength(kept, codeOf(country)) === 'TOO_LONG') {
|
|
92
|
+
kept = kept.slice(0, -1);
|
|
93
|
+
}
|
|
94
|
+
return kept;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Reads what the payer typed or pasted. A national number stays in the
|
|
98
|
+
* country; a full international one (`+225…`, `00225…`) moves to its country
|
|
99
|
+
* when the payment can be paid there, and is kept as typed until its country
|
|
100
|
+
* is known.
|
|
101
|
+
*/
|
|
102
|
+
export function readTypedNumber(raw, country, countries) {
|
|
103
|
+
const text = raw.trim();
|
|
104
|
+
if (text.startsWith('+') || text.startsWith('00')) {
|
|
105
|
+
const international = `+${text.replace(/^(\+|00)/, '').replace(/\D/g, '')}`;
|
|
106
|
+
const typer = new AsYouType();
|
|
107
|
+
typer.input(international);
|
|
108
|
+
const found = typer.getCountry()?.toLowerCase();
|
|
109
|
+
if (found && countries.includes(found)) {
|
|
110
|
+
return {
|
|
111
|
+
country: found,
|
|
112
|
+
// A known country comes with its digits.
|
|
113
|
+
digits: fit(typer.getNumber().nationalNumber, found),
|
|
114
|
+
pending: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { country, digits: '', pending: international };
|
|
118
|
+
}
|
|
119
|
+
const typed = text.replace(/\D/g, '');
|
|
120
|
+
// "237691234567": the dial code typed without its plus.
|
|
121
|
+
const parsed = typed.startsWith(DIAL_CODES[country])
|
|
122
|
+
? parsePhoneNumberFromString(typed, codeOf(country))
|
|
123
|
+
: undefined;
|
|
124
|
+
const digits = parsed?.isValid() ? parsed.nationalNumber : typed;
|
|
125
|
+
return { country, digits: fit(digits, country), pending: null };
|
|
126
|
+
}
|
|
127
|
+
/** "6 91 23 45 67" → "6 91 •• •• 67": keeps the first 3 and last 2 digits. */
|
|
128
|
+
export function maskNumber(formatted) {
|
|
129
|
+
const total = (formatted.match(/\d/g) ?? []).length;
|
|
130
|
+
let seen = 0;
|
|
131
|
+
return formatted.replace(/\d/g, (digit) => {
|
|
132
|
+
seen += 1;
|
|
133
|
+
return seen <= 3 || seen > total - 2 ? digit : '•';
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** Where the caret goes in `formatted` to sit after its `count`-th digit. */
|
|
137
|
+
export function caretAfterDigits(formatted, count) {
|
|
138
|
+
if (count === 0)
|
|
139
|
+
return 0;
|
|
140
|
+
let seen = 0;
|
|
141
|
+
for (let index = 0; index < formatted.length; index++) {
|
|
142
|
+
if (/\d/.test(formatted[index]))
|
|
143
|
+
seen += 1;
|
|
144
|
+
if (seen === count)
|
|
145
|
+
return index + 1;
|
|
146
|
+
}
|
|
147
|
+
return formatted.length;
|
|
148
|
+
}
|
|
149
|
+
/** A USSD code as a link that dials it: `#` must be escaped in a `tel:` URL. */
|
|
150
|
+
export function telHref(code) {
|
|
151
|
+
return `tel:${code.replace(/#/g, '%23')}`;
|
|
152
|
+
}
|
|
153
|
+
/** `+237691234567` → `+237 6 91 •• •• 67`: the dial code, then the number masked. */
|
|
154
|
+
export function maskE164(phone) {
|
|
155
|
+
const parsed = parsePhoneNumberFromString(phone);
|
|
156
|
+
if (!parsed)
|
|
157
|
+
return maskNumber(phone);
|
|
158
|
+
return `+${parsed.countryCallingCode} ${maskNumber(parsed.formatNational())}`;
|
|
159
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CheckoutScreenEnum } from './checkout.enum.js';
|
|
2
|
+
import { CountryEnum } from './domain.enum.js';
|
|
3
|
+
import type { ProviderLogoEnum } from './domain.enum.js';
|
|
4
|
+
import type { CheckoutApp, CheckoutDerived, CheckoutMethod, CheckoutPreviewApp, CheckoutUi, CheckoutView, CountryClues, PhoneStatus, SupportContact } from './checkout.interface.js';
|
|
5
|
+
/** Countries with Mobile Money methods, in the order their first method comes. */
|
|
6
|
+
export declare function countriesOf(methods: Array<CheckoutMethod>): Array<CountryEnum>;
|
|
7
|
+
/** The country whose dial code starts this E.164 number. */
|
|
8
|
+
export declare function countryOfPhone(phone: string): CountryEnum | null;
|
|
9
|
+
/**
|
|
10
|
+
* The payer's country (docs/v1-plan.md, Checkout): the app's hint, then the
|
|
11
|
+
* phone's dial code, then the request's IP, then the browser's language
|
|
12
|
+
* (`fr-CM`). Only countries this payment can be paid in count; with none, the
|
|
13
|
+
* first country the gateway serves, for a page that has nothing to offer.
|
|
14
|
+
*/
|
|
15
|
+
export declare function pickCountry(view: CheckoutView, clues: CountryClues, countries: Array<CountryEnum>): CountryEnum;
|
|
16
|
+
/**
|
|
17
|
+
* How the page opens: on the app's preferred method when it can be paid with,
|
|
18
|
+
* else on its preferred tab, with the list open.
|
|
19
|
+
*/
|
|
20
|
+
export declare function initialUi(view: CheckoutView, clues: CountryClues): CheckoutUi;
|
|
21
|
+
/** Which screen shows: the payment's status first, then what the payer is doing. */
|
|
22
|
+
export declare function screenOf(view: CheckoutView | null | undefined, ui: CheckoutUi): CheckoutScreenEnum;
|
|
23
|
+
/** The brand of a Mobile Money method, which a number's first digits can reveal. */
|
|
24
|
+
export declare function brandOf(method: CheckoutMethod): string;
|
|
25
|
+
/** The tile of a method or an attempt: the generic card for cards, whatever networks they take. */
|
|
26
|
+
export declare function logoOf(of: Pick<CheckoutMethod, 'flow' | 'logos'>): ProviderLogoEnum;
|
|
27
|
+
/** Works out what the screens show from the payment, the page and the number typed. */
|
|
28
|
+
export declare function deriveCheckout(view: CheckoutView, ui: CheckoutUi, phone: PhoneStatus): CheckoutDerived;
|
|
29
|
+
/** Seconds left to approve an attempt started at `startedAt`. */
|
|
30
|
+
export declare function waitLeftOf(startedAt: number, now: number): number;
|
|
31
|
+
/** How payers reach the app's support, when it set any. */
|
|
32
|
+
export declare function supportContactsOf(app: CheckoutApp): Array<SupportContact>;
|
|
33
|
+
/** A pending test payment for the Branding tab's preview, branded as typed. */
|
|
34
|
+
export declare function previewView(app: CheckoutPreviewApp, methods: Array<CheckoutMethod>): CheckoutView;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { CARD_LOGO, DIAL_CODES, OTP_PATTERN, PREVIEW_AMOUNT, PREVIEW_ORDER_REF, WAIT_SECONDS, } from "./checkout.constants.js";
|
|
2
|
+
import { CheckoutPaymentStatusEnum, CheckoutScreenEnum, CheckoutTabEnum, MethodFlowEnum, MethodKindEnum, MethodStateEnum, SupportChannelEnum, } from "./checkout.enum.js";
|
|
3
|
+
import { AttemptStatusEnum, CountryEnum, CurrencyEnum } from "./domain.enum.js";
|
|
4
|
+
/** Countries with Mobile Money methods, in the order their first method comes. */
|
|
5
|
+
export function countriesOf(methods) {
|
|
6
|
+
const countries = methods
|
|
7
|
+
.filter((method) => method.kind === MethodKindEnum.MOBILE_MONEY)
|
|
8
|
+
.map((method) => method.country);
|
|
9
|
+
return [...new Set(countries)];
|
|
10
|
+
}
|
|
11
|
+
/** The country whose dial code starts this E.164 number. */
|
|
12
|
+
export function countryOfPhone(phone) {
|
|
13
|
+
const found = Object.entries(DIAL_CODES).find(([, dial]) => phone.startsWith(`+${dial}`));
|
|
14
|
+
return found ? found[0] : null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The payer's country (docs/v1-plan.md, Checkout): the app's hint, then the
|
|
18
|
+
* phone's dial code, then the request's IP, then the browser's language
|
|
19
|
+
* (`fr-CM`). Only countries this payment can be paid in count; with none, the
|
|
20
|
+
* first country the gateway serves, for a page that has nothing to offer.
|
|
21
|
+
*/
|
|
22
|
+
export function pickCountry(view, clues, countries) {
|
|
23
|
+
const fromLanguages = clues.languages.map((tag) => tag.split('-')[1]?.toLowerCase());
|
|
24
|
+
const candidates = [
|
|
25
|
+
view.country,
|
|
26
|
+
view.payer.phone ? countryOfPhone(view.payer.phone) : null,
|
|
27
|
+
clues.ipCountry?.toLowerCase(),
|
|
28
|
+
...fromLanguages,
|
|
29
|
+
];
|
|
30
|
+
const found = candidates.find((candidate) => countries.includes(candidate));
|
|
31
|
+
return (found ??
|
|
32
|
+
countries.at(0) ??
|
|
33
|
+
Object.values(CountryEnum)[0]);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* How the page opens: on the app's preferred method when it can be paid with,
|
|
37
|
+
* else on its preferred tab, with the list open.
|
|
38
|
+
*/
|
|
39
|
+
export function initialUi(view, clues) {
|
|
40
|
+
const countries = countriesOf(view.methods);
|
|
41
|
+
const preferred = view.methods.find((method) => method.id === view.preferredMethod &&
|
|
42
|
+
method.state === MethodStateEnum.AVAILABLE);
|
|
43
|
+
const base = {
|
|
44
|
+
tab: view.preferredTab ?? CheckoutTabEnum.MOBILE_MONEY,
|
|
45
|
+
country: pickCountry(view, clues, countries),
|
|
46
|
+
method: null,
|
|
47
|
+
code: '',
|
|
48
|
+
cardComplete: false,
|
|
49
|
+
cardPaying: false,
|
|
50
|
+
pickerOpen: true,
|
|
51
|
+
summaryOpen: false,
|
|
52
|
+
submitting: false,
|
|
53
|
+
dismissed: null,
|
|
54
|
+
redirecting: false,
|
|
55
|
+
};
|
|
56
|
+
if (preferred?.kind !== MethodKindEnum.MOBILE_MONEY)
|
|
57
|
+
return base;
|
|
58
|
+
return {
|
|
59
|
+
...base,
|
|
60
|
+
tab: CheckoutTabEnum.MOBILE_MONEY,
|
|
61
|
+
country: preferred.country,
|
|
62
|
+
method: preferred.id,
|
|
63
|
+
pickerOpen: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const TERMINAL_SCREEN = {
|
|
67
|
+
[CheckoutPaymentStatusEnum.PAID]: CheckoutScreenEnum.SUCCESS,
|
|
68
|
+
[CheckoutPaymentStatusEnum.EXPIRED]: CheckoutScreenEnum.EXPIRED,
|
|
69
|
+
[CheckoutPaymentStatusEnum.CANCELLED]: CheckoutScreenEnum.CANCELLED,
|
|
70
|
+
[CheckoutPaymentStatusEnum.LOCKED]: CheckoutScreenEnum.LOCKED,
|
|
71
|
+
};
|
|
72
|
+
/** Which screen shows: the payment's status first, then what the payer is doing. */
|
|
73
|
+
export function screenOf(view, ui) {
|
|
74
|
+
if (view === undefined)
|
|
75
|
+
return CheckoutScreenEnum.LOADING;
|
|
76
|
+
if (view === null)
|
|
77
|
+
return CheckoutScreenEnum.INVALID;
|
|
78
|
+
if (ui.redirecting)
|
|
79
|
+
return CheckoutScreenEnum.REDIRECT;
|
|
80
|
+
const terminal = TERMINAL_SCREEN[view.status];
|
|
81
|
+
if (terminal)
|
|
82
|
+
return terminal;
|
|
83
|
+
const { attempt } = view;
|
|
84
|
+
// A card's fields page is still paying: the form stays until it says how it went.
|
|
85
|
+
const shown = attempt && attempt.reference !== ui.dismissed && !ui.cardPaying
|
|
86
|
+
? attempt.status
|
|
87
|
+
: null;
|
|
88
|
+
if (shown === AttemptStatusEnum.PENDING)
|
|
89
|
+
return CheckoutScreenEnum.WAITING;
|
|
90
|
+
// The app offers nothing any more: no form to fill, no other method to try.
|
|
91
|
+
if (view.methods.length === 0)
|
|
92
|
+
return CheckoutScreenEnum.NO_METHOD;
|
|
93
|
+
if (shown === AttemptStatusEnum.FAILED)
|
|
94
|
+
return CheckoutScreenEnum.FAILED;
|
|
95
|
+
return ui.submitting ? CheckoutScreenEnum.SUBMITTING : CheckoutScreenEnum.FORM;
|
|
96
|
+
}
|
|
97
|
+
/** The brand of a Mobile Money method, which a number's first digits can reveal. */
|
|
98
|
+
export function brandOf(method) {
|
|
99
|
+
return method.logos[0];
|
|
100
|
+
}
|
|
101
|
+
/** Everything the picked method needs is in: the card, or the number and its payment code. */
|
|
102
|
+
function readyToPay(method, ui, phone) {
|
|
103
|
+
if (method.flow === MethodFlowEnum.CARD_FIELDS)
|
|
104
|
+
return ui.cardComplete;
|
|
105
|
+
return (phone.valid &&
|
|
106
|
+
(method.flow !== MethodFlowEnum.OTP || OTP_PATTERN.test(ui.code)));
|
|
107
|
+
}
|
|
108
|
+
/** The tile of a method or an attempt: the generic card for cards, whatever networks they take. */
|
|
109
|
+
export function logoOf(of) {
|
|
110
|
+
return of.flow === MethodFlowEnum.CARD_FIELDS ? CARD_LOGO : of.logos[0];
|
|
111
|
+
}
|
|
112
|
+
/** Works out what the screens show from the payment, the page and the number typed. */
|
|
113
|
+
export function deriveCheckout(view, ui, phone) {
|
|
114
|
+
const wallets = view.methods.filter((method) => method.kind === MethodKindEnum.MOBILE_MONEY &&
|
|
115
|
+
method.country === ui.country);
|
|
116
|
+
const cards = view.methods.filter((method) => method.kind === MethodKindEnum.CARD);
|
|
117
|
+
const onCard = ui.tab === CheckoutTabEnum.CARD;
|
|
118
|
+
const picked = (onCard ? cards : wallets).find((method) => method.id === ui.method);
|
|
119
|
+
const selected = !ui.pickerOpen && picked?.state === MethodStateEnum.AVAILABLE
|
|
120
|
+
? picked
|
|
121
|
+
: null;
|
|
122
|
+
const detected = phone.valid
|
|
123
|
+
? (wallets.find((method) => method.state === MethodStateEnum.AVAILABLE &&
|
|
124
|
+
brandOf(method) === phone.operator) ?? null)
|
|
125
|
+
: null;
|
|
126
|
+
const failed = view.methods.find((method) => method.id === view.attempt?.method);
|
|
127
|
+
const suggestion = view.methods.find((method) => method.kind === MethodKindEnum.MOBILE_MONEY &&
|
|
128
|
+
method.state === MethodStateEnum.AVAILABLE &&
|
|
129
|
+
method.country === failed?.country &&
|
|
130
|
+
method.id !== failed.id) ?? null;
|
|
131
|
+
return {
|
|
132
|
+
screen: screenOf(view, ui),
|
|
133
|
+
countries: countriesOf(view.methods),
|
|
134
|
+
wallets,
|
|
135
|
+
cards,
|
|
136
|
+
selected,
|
|
137
|
+
canPay: !!selected &&
|
|
138
|
+
readyToPay(selected, ui, phone) &&
|
|
139
|
+
!ui.submitting &&
|
|
140
|
+
!ui.cardPaying,
|
|
141
|
+
// Only a Mobile Money method has a number to belong to another one.
|
|
142
|
+
mismatch: selected && !onCard && detected && detected.id !== selected.id
|
|
143
|
+
? detected
|
|
144
|
+
: null,
|
|
145
|
+
suggestion,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** Seconds left to approve an attempt started at `startedAt`. */
|
|
149
|
+
export function waitLeftOf(startedAt, now) {
|
|
150
|
+
const elapsed = Math.floor((now - startedAt) / 1000);
|
|
151
|
+
return Math.min(WAIT_SECONDS, Math.max(0, WAIT_SECONDS - elapsed));
|
|
152
|
+
}
|
|
153
|
+
/** How payers reach the app's support, when it set any. */
|
|
154
|
+
export function supportContactsOf(app) {
|
|
155
|
+
const contacts = [];
|
|
156
|
+
if (app.supportWhatsapp) {
|
|
157
|
+
contacts.push({
|
|
158
|
+
channel: SupportChannelEnum.WHATSAPP,
|
|
159
|
+
value: app.supportWhatsapp,
|
|
160
|
+
href: `https://wa.me/${app.supportWhatsapp.slice(1)}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (app.supportEmail) {
|
|
164
|
+
contacts.push({
|
|
165
|
+
channel: SupportChannelEnum.EMAIL,
|
|
166
|
+
value: app.supportEmail,
|
|
167
|
+
href: `mailto:${app.supportEmail}`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return contacts;
|
|
171
|
+
}
|
|
172
|
+
/** A pending test payment for the Branding tab's preview, branded as typed. */
|
|
173
|
+
export function previewView(app, methods) {
|
|
174
|
+
return {
|
|
175
|
+
id: 'preview',
|
|
176
|
+
status: CheckoutPaymentStatusEnum.PENDING,
|
|
177
|
+
test: false,
|
|
178
|
+
amount: PREVIEW_AMOUNT,
|
|
179
|
+
currency: CurrencyEnum.XAF,
|
|
180
|
+
orderRef: PREVIEW_ORDER_REF,
|
|
181
|
+
items: [],
|
|
182
|
+
returnUrl: '',
|
|
183
|
+
expiresAt: 0,
|
|
184
|
+
paidAt: null,
|
|
185
|
+
payer: { name: null, phone: null, email: null },
|
|
186
|
+
country: null,
|
|
187
|
+
language: null,
|
|
188
|
+
theme: null,
|
|
189
|
+
preferredMethod: null,
|
|
190
|
+
preferredTab: null,
|
|
191
|
+
attemptsLeft: 0,
|
|
192
|
+
maxAttempts: 0,
|
|
193
|
+
// The preview offers no card: nothing loads from the site.
|
|
194
|
+
siteUrl: '',
|
|
195
|
+
app: {
|
|
196
|
+
...app,
|
|
197
|
+
termsUrl: null,
|
|
198
|
+
privacyUrl: null,
|
|
199
|
+
supportEmail: null,
|
|
200
|
+
supportWhatsapp: null,
|
|
201
|
+
},
|
|
202
|
+
methods,
|
|
203
|
+
attempt: null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Domain enums the checkout core needs; `src/models` re-exports them for the rest of the app. */
|
|
2
|
+
/** Values are the file names in `public/logo/`, without the extension. */
|
|
3
|
+
/** A brand tile in `public/logo/`: one per brand a payer recognises (see `docs/design-system.md`, Logos). */
|
|
4
|
+
export declare enum ProviderLogoEnum {
|
|
5
|
+
ORANGE = "orange",
|
|
6
|
+
MTN = "mtn",
|
|
7
|
+
MOOV = "moov",
|
|
8
|
+
WAVE = "wave",
|
|
9
|
+
MIXX = "mixx",
|
|
10
|
+
VISA = "visa",
|
|
11
|
+
MASTERCARD = "mastercard",
|
|
12
|
+
PAYPAL = "paypal",
|
|
13
|
+
CARD = "card",
|
|
14
|
+
APPLE_PAY = "apple-pay",
|
|
15
|
+
GOOGLE_PAY = "google-pay"
|
|
16
|
+
}
|
|
17
|
+
/** Declared in the order the Providers page lists its country groups. */
|
|
18
|
+
export declare enum CountryEnum {
|
|
19
|
+
CAMEROON = "cm",
|
|
20
|
+
COTE_D_IVOIRE = "ci",
|
|
21
|
+
SENEGAL = "sn",
|
|
22
|
+
BENIN = "bj",
|
|
23
|
+
TOGO = "tg",
|
|
24
|
+
BURKINA_FASO = "bf",
|
|
25
|
+
INTERNATIONAL = "intl"
|
|
26
|
+
}
|
|
27
|
+
export declare enum CurrencyEnum {
|
|
28
|
+
XAF = "XAF",
|
|
29
|
+
XOF = "XOF",
|
|
30
|
+
EUR = "EUR",
|
|
31
|
+
USD = "USD"
|
|
32
|
+
}
|
|
33
|
+
/** Why an attempt failed. The first five are the payer's and count toward the lock. */
|
|
34
|
+
export declare enum FailureReasonEnum {
|
|
35
|
+
DECLINED = "declined",
|
|
36
|
+
INSUFFICIENT_FUNDS = "insufficient_funds",
|
|
37
|
+
/** No mobile money account on this number. */
|
|
38
|
+
NO_ACCOUNT = "no_account",
|
|
39
|
+
WRONG_PIN = "wrong_pin",
|
|
40
|
+
/** An OTP method: the payment code was wrong or had expired. */
|
|
41
|
+
WRONG_CODE = "wrong_code",
|
|
42
|
+
PROVIDER_ERROR = "provider_error",
|
|
43
|
+
TIMEOUT = "timeout",
|
|
44
|
+
MISMATCH = "mismatch"
|
|
45
|
+
}
|
|
46
|
+
/** GLOSSARY: Attempt. */
|
|
47
|
+
export declare enum AttemptStatusEnum {
|
|
48
|
+
PENDING = "pending",
|
|
49
|
+
SUCCEEDED = "succeeded",
|
|
50
|
+
FAILED = "failed"
|
|
51
|
+
}
|
|
52
|
+
/** Interface languages. Values are the URL prefix and the saved cookie value. */
|
|
53
|
+
export declare enum LangEnum {
|
|
54
|
+
FR = "fr",
|
|
55
|
+
EN = "en"
|
|
56
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Domain enums the checkout core needs; `src/models` re-exports them for the rest of the app. */
|
|
2
|
+
/** Values are the file names in `public/logo/`, without the extension. */
|
|
3
|
+
/** A brand tile in `public/logo/`: one per brand a payer recognises (see `docs/design-system.md`, Logos). */
|
|
4
|
+
export var ProviderLogoEnum;
|
|
5
|
+
(function (ProviderLogoEnum) {
|
|
6
|
+
ProviderLogoEnum["ORANGE"] = "orange";
|
|
7
|
+
ProviderLogoEnum["MTN"] = "mtn";
|
|
8
|
+
ProviderLogoEnum["MOOV"] = "moov";
|
|
9
|
+
ProviderLogoEnum["WAVE"] = "wave";
|
|
10
|
+
ProviderLogoEnum["MIXX"] = "mixx";
|
|
11
|
+
ProviderLogoEnum["VISA"] = "visa";
|
|
12
|
+
ProviderLogoEnum["MASTERCARD"] = "mastercard";
|
|
13
|
+
ProviderLogoEnum["PAYPAL"] = "paypal";
|
|
14
|
+
ProviderLogoEnum["CARD"] = "card";
|
|
15
|
+
ProviderLogoEnum["APPLE_PAY"] = "apple-pay";
|
|
16
|
+
ProviderLogoEnum["GOOGLE_PAY"] = "google-pay";
|
|
17
|
+
})(ProviderLogoEnum || (ProviderLogoEnum = {}));
|
|
18
|
+
/** Declared in the order the Providers page lists its country groups. */
|
|
19
|
+
export var CountryEnum;
|
|
20
|
+
(function (CountryEnum) {
|
|
21
|
+
CountryEnum["CAMEROON"] = "cm";
|
|
22
|
+
CountryEnum["COTE_D_IVOIRE"] = "ci";
|
|
23
|
+
CountryEnum["SENEGAL"] = "sn";
|
|
24
|
+
CountryEnum["BENIN"] = "bj";
|
|
25
|
+
CountryEnum["TOGO"] = "tg";
|
|
26
|
+
CountryEnum["BURKINA_FASO"] = "bf";
|
|
27
|
+
CountryEnum["INTERNATIONAL"] = "intl";
|
|
28
|
+
})(CountryEnum || (CountryEnum = {}));
|
|
29
|
+
export var CurrencyEnum;
|
|
30
|
+
(function (CurrencyEnum) {
|
|
31
|
+
CurrencyEnum["XAF"] = "XAF";
|
|
32
|
+
CurrencyEnum["XOF"] = "XOF";
|
|
33
|
+
CurrencyEnum["EUR"] = "EUR";
|
|
34
|
+
CurrencyEnum["USD"] = "USD";
|
|
35
|
+
})(CurrencyEnum || (CurrencyEnum = {}));
|
|
36
|
+
/** Why an attempt failed. The first five are the payer's and count toward the lock. */
|
|
37
|
+
export var FailureReasonEnum;
|
|
38
|
+
(function (FailureReasonEnum) {
|
|
39
|
+
FailureReasonEnum["DECLINED"] = "declined";
|
|
40
|
+
FailureReasonEnum["INSUFFICIENT_FUNDS"] = "insufficient_funds";
|
|
41
|
+
/** No mobile money account on this number. */
|
|
42
|
+
FailureReasonEnum["NO_ACCOUNT"] = "no_account";
|
|
43
|
+
FailureReasonEnum["WRONG_PIN"] = "wrong_pin";
|
|
44
|
+
/** An OTP method: the payment code was wrong or had expired. */
|
|
45
|
+
FailureReasonEnum["WRONG_CODE"] = "wrong_code";
|
|
46
|
+
FailureReasonEnum["PROVIDER_ERROR"] = "provider_error";
|
|
47
|
+
FailureReasonEnum["TIMEOUT"] = "timeout";
|
|
48
|
+
FailureReasonEnum["MISMATCH"] = "mismatch";
|
|
49
|
+
})(FailureReasonEnum || (FailureReasonEnum = {}));
|
|
50
|
+
/** GLOSSARY: Attempt. */
|
|
51
|
+
export var AttemptStatusEnum;
|
|
52
|
+
(function (AttemptStatusEnum) {
|
|
53
|
+
AttemptStatusEnum["PENDING"] = "pending";
|
|
54
|
+
AttemptStatusEnum["SUCCEEDED"] = "succeeded";
|
|
55
|
+
AttemptStatusEnum["FAILED"] = "failed";
|
|
56
|
+
})(AttemptStatusEnum || (AttemptStatusEnum = {}));
|
|
57
|
+
/** Interface languages. Values are the URL prefix and the saved cookie value. */
|
|
58
|
+
export var LangEnum;
|
|
59
|
+
(function (LangEnum) {
|
|
60
|
+
LangEnum["FR"] = "fr";
|
|
61
|
+
LangEnum["EN"] = "en";
|
|
62
|
+
})(LangEnum || (LangEnum = {}));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BrissotoPayProviderProps } from '../models/sheet.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Around the app: holds the connection to Brissoto Pay and the one payment
|
|
4
|
+
* sheet `usePaymentSheet().presentPayment` opens.
|
|
5
|
+
*/
|
|
6
|
+
export declare function BrissotoPayProvider({ url, children, }: BrissotoPayProviderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ConvexReactClient } from 'convex/react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { PaymentSheetContext } from "../hooks/use-payment-sheet.js";
|
|
5
|
+
import { DEFAULT_URL } from "../models/sheet.constants.js";
|
|
6
|
+
import { convexCheckoutCalls } from "../services/checkout.service.js";
|
|
7
|
+
import { PaymentSheetModal } from "./PaymentSheetModal.js";
|
|
8
|
+
/**
|
|
9
|
+
* Around the app: holds the connection to Brissoto Pay and the one payment
|
|
10
|
+
* sheet `usePaymentSheet().presentPayment` opens.
|
|
11
|
+
*/
|
|
12
|
+
export function BrissotoPayProvider({ url = DEFAULT_URL, children, }) {
|
|
13
|
+
const [client, setClient] = useState(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const next = new ConvexReactClient(url, { unsavedChangesWarning: false });
|
|
16
|
+
setClient(next);
|
|
17
|
+
return () => void next.close();
|
|
18
|
+
}, [url]);
|
|
19
|
+
const calls = useMemo(() => client && convexCheckoutCalls(client), [client]);
|
|
20
|
+
const [request, setRequest] = useState(null);
|
|
21
|
+
/** Sheets opened, so each gets a fresh one. */
|
|
22
|
+
const [count, setCount] = useState(0);
|
|
23
|
+
const busy = useRef(false);
|
|
24
|
+
const presentPayment = useCallback(({ paymentId }) => {
|
|
25
|
+
if (busy.current) {
|
|
26
|
+
return Promise.reject(new Error('A payment sheet is already open.'));
|
|
27
|
+
}
|
|
28
|
+
busy.current = true;
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
setCount((previous) => previous + 1);
|
|
31
|
+
setRequest({ paymentId, open: true, resolve });
|
|
32
|
+
});
|
|
33
|
+
}, []);
|
|
34
|
+
const sheet = useMemo(() => ({ presentPayment }), [presentPayment]);
|
|
35
|
+
const close = (closing, result) => {
|
|
36
|
+
busy.current = false;
|
|
37
|
+
setRequest({ ...closing, open: false });
|
|
38
|
+
closing.resolve(result);
|
|
39
|
+
};
|
|
40
|
+
return (_jsxs(PaymentSheetContext, { value: sheet, children: [children, calls && request && (_jsx(PaymentSheetModal, { calls: calls, request: request, onClose: (result) => close(request, result) }, count))] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PaymentSheetModalProps } from '../models/sheet.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* One payment in a bottom sheet over the dimmed app, as tall as what it
|
|
4
|
+
* shows, in the payment's language and theme, else the device's. Close,
|
|
5
|
+
* a swipe down, a tap on the app behind, back or escape leave it; leaving
|
|
6
|
+
* while a prompt waits on the phone asks first, and resolves `pending`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function PaymentSheetModal({ calls, request, onClose, }: PaymentSheetModalProps): import("react").JSX.Element;
|