@heyama/pay-react-native 0.0.0-stage → 1.0.1
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 +253 -0
- package/dist/checkout-core/messages/en.js +243 -0
- package/dist/checkout-core/messages/fr.d.ts +4 -0
- package/dist/checkout-core/messages/fr.js +245 -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 +133 -0
- package/dist/checkout-core/models/checkout.enum.js +150 -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 +77 -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/SheetChosenMethod.d.ts +3 -0
- package/dist/components/SheetChosenMethod.js +60 -0
- package/dist/components/SheetCodeField.d.ts +8 -0
- package/dist/components/SheetCodeField.js +97 -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 +8 -0
- package/dist/components/SheetForm.js +101 -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 +9 -0
- package/dist/components/SheetLayout.js +48 -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 +32 -0
- package/dist/components/SheetPhoneField.d.ts +3 -0
- package/dist/components/SheetPhoneField.js +58 -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 +232 -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,417 @@
|
|
|
1
|
+
import type { CardFieldsMessageEnum, CheckoutPaymentStatusEnum, CheckoutScreenEnum, CheckoutTabEnum, CheckoutThemeEnum, MethodBrandEnum, MethodFlowEnum, MethodKindEnum, MethodStateEnum, PayRefusalEnum, PhoneErrorEnum, SimulatorOutcomeEnum, SupportChannelEnum } from './checkout.enum.js';
|
|
2
|
+
import type { AttemptStatusEnum, CountryEnum, CurrencyEnum, FailureReasonEnum, LangEnum, ProviderLogoEnum } from './domain.enum.js';
|
|
3
|
+
export interface CheckoutApp {
|
|
4
|
+
name: string;
|
|
5
|
+
color: string;
|
|
6
|
+
logoUrl: string | null;
|
|
7
|
+
termsUrl: string | null;
|
|
8
|
+
privacyUrl: string | null;
|
|
9
|
+
supportEmail: string | null;
|
|
10
|
+
/** E.164. */
|
|
11
|
+
supportWhatsapp: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface CheckoutItem {
|
|
14
|
+
name: string;
|
|
15
|
+
quantity: number;
|
|
16
|
+
/** Minor units. */
|
|
17
|
+
unitAmount: number;
|
|
18
|
+
}
|
|
19
|
+
/** A payment method this payment can be paid with, in the admin's order. */
|
|
20
|
+
export interface CheckoutMethod {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
kind: MethodKindEnum;
|
|
24
|
+
country: CountryEnum;
|
|
25
|
+
/** The network that runs it ("Orange Cameroun"). */
|
|
26
|
+
network: string;
|
|
27
|
+
flow: MethodFlowEnum;
|
|
28
|
+
/** The code a payer dials to see payments waiting for approval. */
|
|
29
|
+
pendingCode: string | null;
|
|
30
|
+
/** OTP methods: what the payer dials to get a payment code for this amount. */
|
|
31
|
+
otpCode: string | null;
|
|
32
|
+
/** Card methods: the provider's page hosting its card fields, shown in an iframe. */
|
|
33
|
+
cardFieldsPath: string | null;
|
|
34
|
+
logos: Array<ProviderLogoEnum>;
|
|
35
|
+
state: MethodStateEnum;
|
|
36
|
+
/** Why it is in maintenance, for the payer. */
|
|
37
|
+
note: string | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A paid card as its provider reported it: never more than the last 4
|
|
41
|
+
* digits. The brand is the provider's own word, whatever the network.
|
|
42
|
+
*/
|
|
43
|
+
export interface CardSummary {
|
|
44
|
+
brand: string;
|
|
45
|
+
last4: string;
|
|
46
|
+
}
|
|
47
|
+
export interface CheckoutAttempt {
|
|
48
|
+
reference: string;
|
|
49
|
+
method: string;
|
|
50
|
+
/** As the payer read it when paying. */
|
|
51
|
+
methodName: string;
|
|
52
|
+
logos: Array<ProviderLogoEnum>;
|
|
53
|
+
flow: MethodFlowEnum;
|
|
54
|
+
pendingCode: string | null;
|
|
55
|
+
status: AttemptStatusEnum;
|
|
56
|
+
failureReason: FailureReasonEnum | null;
|
|
57
|
+
/** E.164; a card has none. */
|
|
58
|
+
phone: string | null;
|
|
59
|
+
/** A paid card: its brand and last 4 digits. */
|
|
60
|
+
card: CardSummary | null;
|
|
61
|
+
startedAt: number;
|
|
62
|
+
/** A test attempt the Simulator took: the tester picks its outcome. */
|
|
63
|
+
simulated: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface CheckoutPayer {
|
|
66
|
+
name: string | null;
|
|
67
|
+
phone: string | null;
|
|
68
|
+
email: string | null;
|
|
69
|
+
}
|
|
70
|
+
/** One payment as the checkout shows it (`checkout.queries.get`). */
|
|
71
|
+
export interface CheckoutView {
|
|
72
|
+
id: string;
|
|
73
|
+
status: CheckoutPaymentStatusEnum;
|
|
74
|
+
test: boolean;
|
|
75
|
+
/** Minor units. */
|
|
76
|
+
amount: number;
|
|
77
|
+
currency: CurrencyEnum;
|
|
78
|
+
orderRef: string | null;
|
|
79
|
+
items: Array<CheckoutItem>;
|
|
80
|
+
returnUrl: string;
|
|
81
|
+
expiresAt: number;
|
|
82
|
+
paidAt: number | null;
|
|
83
|
+
payer: CheckoutPayer;
|
|
84
|
+
/** The app's hint for the payer's country. */
|
|
85
|
+
country: CountryEnum | null;
|
|
86
|
+
language: LangEnum | null;
|
|
87
|
+
theme: CheckoutThemeEnum | null;
|
|
88
|
+
preferredMethod: string | null;
|
|
89
|
+
preferredTab: CheckoutTabEnum | null;
|
|
90
|
+
app: CheckoutApp;
|
|
91
|
+
methods: Array<CheckoutMethod>;
|
|
92
|
+
/** Payer-caused failures it still takes before it locks, out of `maxAttempts`. */
|
|
93
|
+
attemptsLeft: number;
|
|
94
|
+
maxAttempts: number;
|
|
95
|
+
attempt: CheckoutAttempt | null;
|
|
96
|
+
/**
|
|
97
|
+
* Where the checkout is served, without a trailing slash: the payment
|
|
98
|
+
* sheet loads card fields pages (`cardFieldsPath`) from it.
|
|
99
|
+
*/
|
|
100
|
+
siteUrl: string;
|
|
101
|
+
}
|
|
102
|
+
/** Where the request came from, for picking the payer's country. */
|
|
103
|
+
export interface CountryClues {
|
|
104
|
+
/** From the proxy's country header, when it sends one. */
|
|
105
|
+
ipCountry: string | null;
|
|
106
|
+
/** Browser languages in preference order (`fr-CM`). */
|
|
107
|
+
languages: Array<string>;
|
|
108
|
+
}
|
|
109
|
+
/** A greyed row on the Card tab for a wallet no provider offers yet. */
|
|
110
|
+
export interface ComingSoonWallet {
|
|
111
|
+
/** Catalog key under `wallets` and logo name. */
|
|
112
|
+
id: ProviderLogoEnum;
|
|
113
|
+
}
|
|
114
|
+
export interface OperatorDetection {
|
|
115
|
+
/** The operator the number belongs to, once enough digits say so. */
|
|
116
|
+
operator: MethodBrandEnum | null;
|
|
117
|
+
/** Enough digits were typed and no known operator matches. */
|
|
118
|
+
unrecognized: boolean;
|
|
119
|
+
}
|
|
120
|
+
export interface PhoneInputs {
|
|
121
|
+
country: CountryEnum;
|
|
122
|
+
/** Digits of the national number, no dial code and no spaces. */
|
|
123
|
+
digits: string;
|
|
124
|
+
/** A full international number of a country the payment is not offered in. */
|
|
125
|
+
foreign: boolean;
|
|
126
|
+
touched: boolean;
|
|
127
|
+
focused: boolean;
|
|
128
|
+
submitTried: boolean;
|
|
129
|
+
}
|
|
130
|
+
export interface PhoneStatus {
|
|
131
|
+
error: PhoneErrorEnum | null;
|
|
132
|
+
valid: boolean;
|
|
133
|
+
operator: MethodBrandEnum | null;
|
|
134
|
+
}
|
|
135
|
+
/** What the payer typed, read: its country and national digits. */
|
|
136
|
+
export interface TypedNumber {
|
|
137
|
+
country: CountryEnum;
|
|
138
|
+
digits: string;
|
|
139
|
+
/** A `+…` number kept as typed while its country is unknown or not offered. */
|
|
140
|
+
pending: string | null;
|
|
141
|
+
}
|
|
142
|
+
export interface PhoneNumberOptions {
|
|
143
|
+
country: CountryEnum;
|
|
144
|
+
/** Countries of this payment's Mobile Money methods. */
|
|
145
|
+
countries: Array<CountryEnum>;
|
|
146
|
+
/** The payer typed or pasted a full number of another offered country. */
|
|
147
|
+
onCountryChange: (country: CountryEnum) => void;
|
|
148
|
+
}
|
|
149
|
+
/** The Mobile Money number without its field: each platform draws and wires its own input. */
|
|
150
|
+
export interface PhoneNumber {
|
|
151
|
+
/** What the field shows: the number grouped the country's way. */
|
|
152
|
+
value: string;
|
|
153
|
+
/**
|
|
154
|
+
* The field's new text and where its caret was; answers where the caret
|
|
155
|
+
* goes in the grouped number, or null to leave it (an international number).
|
|
156
|
+
*/
|
|
157
|
+
type: (text: string, caret: number) => number | null;
|
|
158
|
+
/** E.164, empty until the number is complete. */
|
|
159
|
+
e164: string;
|
|
160
|
+
dial: string;
|
|
161
|
+
placeholder: string;
|
|
162
|
+
status: PhoneStatus;
|
|
163
|
+
focused: boolean;
|
|
164
|
+
onFocus: () => void;
|
|
165
|
+
onBlur: () => void;
|
|
166
|
+
/** Marks the field as submitted, so its error shows. */
|
|
167
|
+
reject: () => void;
|
|
168
|
+
/** The app's number for the payer, when it is of the page's country. */
|
|
169
|
+
fill: (e164: string) => void;
|
|
170
|
+
}
|
|
171
|
+
/** What the payer did on this page; the payment itself comes from the gateway. */
|
|
172
|
+
export interface CheckoutUi {
|
|
173
|
+
tab: CheckoutTabEnum;
|
|
174
|
+
country: CountryEnum;
|
|
175
|
+
/** The Mobile Money method picked, if any. */
|
|
176
|
+
method: string | null;
|
|
177
|
+
/** The payment code typed for an OTP method; cleared when the method changes. */
|
|
178
|
+
code: string;
|
|
179
|
+
/** A card method's fields say the card is complete. */
|
|
180
|
+
cardComplete: boolean;
|
|
181
|
+
/** Pay went to the card fields page: the form stays until it says how it went. */
|
|
182
|
+
cardPaying: boolean;
|
|
183
|
+
pickerOpen: boolean;
|
|
184
|
+
summaryOpen: boolean;
|
|
185
|
+
submitting: boolean;
|
|
186
|
+
/** An attempt the payer walked away from: the form shows again. */
|
|
187
|
+
dismissed: string | null;
|
|
188
|
+
redirecting: boolean;
|
|
189
|
+
}
|
|
190
|
+
/** Everything the screens show that follows from the payment and the page, worked out once. */
|
|
191
|
+
export interface CheckoutDerived {
|
|
192
|
+
screen: CheckoutScreenEnum;
|
|
193
|
+
/** Countries with Mobile Money methods, in the admin's order. */
|
|
194
|
+
countries: Array<CountryEnum>;
|
|
195
|
+
wallets: Array<CheckoutMethod>;
|
|
196
|
+
cards: Array<CheckoutMethod>;
|
|
197
|
+
/** The picked method, while it can still be used. */
|
|
198
|
+
selected: CheckoutMethod | null;
|
|
199
|
+
canPay: boolean;
|
|
200
|
+
/** The number belongs to another method of the country than the one picked. */
|
|
201
|
+
mismatch: CheckoutMethod | null;
|
|
202
|
+
/** After a failure: another method of the same country the payer can use now. */
|
|
203
|
+
suggestion: CheckoutMethod | null;
|
|
204
|
+
}
|
|
205
|
+
export interface CheckoutCoreActions {
|
|
206
|
+
toggleSummary: () => void;
|
|
207
|
+
setTab: (tab: CheckoutTabEnum) => void;
|
|
208
|
+
setCountry: (country: CountryEnum) => void;
|
|
209
|
+
selectMethod: (method: CheckoutMethod) => void;
|
|
210
|
+
/** A greyed wallet no provider offers yet was tapped. */
|
|
211
|
+
comingSoon: (name: string) => void;
|
|
212
|
+
openPicker: () => void;
|
|
213
|
+
switchToDetected: () => void;
|
|
214
|
+
pay: () => void;
|
|
215
|
+
/** The failed attempt again: same method, same number. An OTP method asks for a new code first. */
|
|
216
|
+
retry: () => void;
|
|
217
|
+
setCode: (code: string) => void;
|
|
218
|
+
/** The phone could not open its dialler on the OTP method's code: dial it by hand. */
|
|
219
|
+
cannotDial: () => void;
|
|
220
|
+
/** Leaves the failure screen for the suggested method, or the list. */
|
|
221
|
+
useAnother: () => void;
|
|
222
|
+
/** "Didn't get it?": the prompt again. */
|
|
223
|
+
resend: () => void;
|
|
224
|
+
cancel: () => void;
|
|
225
|
+
returnNow: () => void;
|
|
226
|
+
stayHere: () => void;
|
|
227
|
+
openHelp: () => void;
|
|
228
|
+
closeHelp: () => void;
|
|
229
|
+
copyReference: () => void;
|
|
230
|
+
downloadReceipt: () => void;
|
|
231
|
+
simulate: (outcome: SimulatorOutcomeEnum) => void;
|
|
232
|
+
}
|
|
233
|
+
/** The checkout's state and actions, the same on the web page and in the payment sheet. */
|
|
234
|
+
export interface CheckoutCore {
|
|
235
|
+
/** Null for a link that matches no payment. */
|
|
236
|
+
view: CheckoutView | null;
|
|
237
|
+
ui: CheckoutUi;
|
|
238
|
+
derived: CheckoutDerived;
|
|
239
|
+
phone: PhoneNumber;
|
|
240
|
+
/** Seconds left on the waiting screen's clock. */
|
|
241
|
+
waitLeft: number;
|
|
242
|
+
/** Seconds before the success screen sends the payer back, null once stopped. */
|
|
243
|
+
returnLeft: number | null;
|
|
244
|
+
helpOpen: boolean;
|
|
245
|
+
toastMessage: string | null;
|
|
246
|
+
copied: boolean;
|
|
247
|
+
/** The Branding tab's preview: nothing can be paid. */
|
|
248
|
+
preview: boolean;
|
|
249
|
+
/** The card fields' content height, as their page last said; null until it loaded. */
|
|
250
|
+
cardHeight: number | null;
|
|
251
|
+
actions: CheckoutCoreActions;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* What only the platform can do, handed to the controller. Pass one stable
|
|
255
|
+
* object: a new one each render subscribes again.
|
|
256
|
+
*/
|
|
257
|
+
export interface CheckoutPlatform {
|
|
258
|
+
/** Calls `listener` each time the payer comes back to the page or the app; answers how to stop. */
|
|
259
|
+
onReturn: (listener: () => void) => () => void;
|
|
260
|
+
copyText: (text: string) => void;
|
|
261
|
+
/** Leaves for the app's return URL. */
|
|
262
|
+
openUrl: (url: string) => void;
|
|
263
|
+
/** Calls `listener` with each message the card fields page sends; answers how to stop. */
|
|
264
|
+
onCardMessage: (listener: (message: CardFieldsMessage) => void) => () => void;
|
|
265
|
+
/** Tells the card fields page that Pay was pressed. */
|
|
266
|
+
submitCard: () => void;
|
|
267
|
+
/** Hands the receipt to the payer as a text file. */
|
|
268
|
+
saveReceipt: (fileName: string, text: string) => void;
|
|
269
|
+
/** Pay found the number wrong: draw the payer's eye to the field. */
|
|
270
|
+
onPhoneRejected: () => void;
|
|
271
|
+
}
|
|
272
|
+
export interface CheckoutControllerOptions {
|
|
273
|
+
clues: CountryClues;
|
|
274
|
+
/** Null on the Branding tab's preview: nothing is paid there. */
|
|
275
|
+
mutations: CheckoutMutations | null;
|
|
276
|
+
platform: CheckoutPlatform;
|
|
277
|
+
}
|
|
278
|
+
/** What Pay answers: refused with a reason, or an attempt started. */
|
|
279
|
+
export interface PayResult {
|
|
280
|
+
ok: boolean;
|
|
281
|
+
reason?: PayRefusalEnum;
|
|
282
|
+
}
|
|
283
|
+
/** The gateway calls the page makes; none on the Branding tab's preview. */
|
|
284
|
+
export interface CheckoutMutations {
|
|
285
|
+
pay: (args: {
|
|
286
|
+
paymentId: string;
|
|
287
|
+
method: string;
|
|
288
|
+
phone: string;
|
|
289
|
+
}) => Promise<PayResult>;
|
|
290
|
+
/** OTP methods: Pay with the payment code, which the server never stores. */
|
|
291
|
+
payWithCode: (args: {
|
|
292
|
+
paymentId: string;
|
|
293
|
+
method: string;
|
|
294
|
+
phone: string;
|
|
295
|
+
otp: string;
|
|
296
|
+
}) => Promise<PayResult>;
|
|
297
|
+
checkNow: (args: {
|
|
298
|
+
paymentId: string;
|
|
299
|
+
}) => Promise<null>;
|
|
300
|
+
/** The payer opened the link; the funnel counts the first time only. */
|
|
301
|
+
open: (args: {
|
|
302
|
+
paymentId: string;
|
|
303
|
+
}) => Promise<null>;
|
|
304
|
+
cancel: (args: {
|
|
305
|
+
paymentId: string;
|
|
306
|
+
}) => Promise<null>;
|
|
307
|
+
resend: (args: {
|
|
308
|
+
paymentId: string;
|
|
309
|
+
}) => Promise<{
|
|
310
|
+
ok: boolean;
|
|
311
|
+
}>;
|
|
312
|
+
simulate: (args: {
|
|
313
|
+
paymentId: string;
|
|
314
|
+
outcome: SimulatorOutcomeEnum;
|
|
315
|
+
}) => Promise<null>;
|
|
316
|
+
}
|
|
317
|
+
/** What the payer typed in the Simulator's stand-in card fields; it never leaves that page. */
|
|
318
|
+
export interface CardEntry {
|
|
319
|
+
number: string;
|
|
320
|
+
expiry: string;
|
|
321
|
+
cvc: string;
|
|
322
|
+
}
|
|
323
|
+
/** A card fields page's address: the payment, its method, and the look of the checkout that shows it. */
|
|
324
|
+
export interface CardFieldsSearch {
|
|
325
|
+
payment?: string;
|
|
326
|
+
method?: string;
|
|
327
|
+
/** The app colour, a hex code. */
|
|
328
|
+
color?: string;
|
|
329
|
+
theme?: CheckoutThemeEnum.DARK;
|
|
330
|
+
}
|
|
331
|
+
/** The Simulator's stand-in card fields page, from its address. */
|
|
332
|
+
export interface SimulatorCardFieldsProps {
|
|
333
|
+
/** Null when the page is opened on its own: nothing can be paid. */
|
|
334
|
+
paymentId: string | null;
|
|
335
|
+
method: string | null;
|
|
336
|
+
/** The app colour, a hex code. */
|
|
337
|
+
color: string | null;
|
|
338
|
+
dark: boolean;
|
|
339
|
+
}
|
|
340
|
+
/** What a card fields page and the checkout send each other (`CardFieldsMessageEnum`). */
|
|
341
|
+
export interface CardFieldsMessage {
|
|
342
|
+
type: CardFieldsMessageEnum;
|
|
343
|
+
complete?: boolean;
|
|
344
|
+
height?: number;
|
|
345
|
+
reason?: PayRefusalEnum;
|
|
346
|
+
}
|
|
347
|
+
/** How a Pay goes: the number, and the payment code of an OTP method. */
|
|
348
|
+
export interface PayVia {
|
|
349
|
+
phone: string;
|
|
350
|
+
otp?: string;
|
|
351
|
+
}
|
|
352
|
+
/** The Convex calls of the stand-in card fields page. */
|
|
353
|
+
export interface SimulatorCardFieldsCalls {
|
|
354
|
+
startCard: (args: {
|
|
355
|
+
paymentId: string;
|
|
356
|
+
method: string;
|
|
357
|
+
}) => Promise<{
|
|
358
|
+
ok: true;
|
|
359
|
+
reference: string;
|
|
360
|
+
providerReference: string | null;
|
|
361
|
+
} | {
|
|
362
|
+
ok: false;
|
|
363
|
+
reason: PayRefusalEnum;
|
|
364
|
+
}>;
|
|
365
|
+
receiveCard: (args: {
|
|
366
|
+
reference: string;
|
|
367
|
+
brand: string;
|
|
368
|
+
last4: string;
|
|
369
|
+
}) => Promise<null>;
|
|
370
|
+
checkNow: (args: {
|
|
371
|
+
paymentId: string;
|
|
372
|
+
}) => Promise<null>;
|
|
373
|
+
}
|
|
374
|
+
/** What each way of approving changes on the screens. */
|
|
375
|
+
export interface FlowTraits {
|
|
376
|
+
/** The tab its methods are on. */
|
|
377
|
+
tab: CheckoutTabEnum;
|
|
378
|
+
/** A prompt on the phone: the waiting screen's steps and "send it again". */
|
|
379
|
+
prompt: boolean;
|
|
380
|
+
/** What the Simulator offers that this flow's payer cannot do. */
|
|
381
|
+
hiddenOutcomes: Array<SimulatorOutcomeEnum>;
|
|
382
|
+
}
|
|
383
|
+
/** The checkout's themed colours (`--ck-*` in `checkout.css`), for a platform without the stylesheet. */
|
|
384
|
+
export interface CheckoutColors {
|
|
385
|
+
page: string;
|
|
386
|
+
surface: string;
|
|
387
|
+
tabOn: string;
|
|
388
|
+
fill: string;
|
|
389
|
+
fill2: string;
|
|
390
|
+
fill3: string;
|
|
391
|
+
ink: string;
|
|
392
|
+
inkStrong: string;
|
|
393
|
+
ink2: string;
|
|
394
|
+
muted: string;
|
|
395
|
+
faint: string;
|
|
396
|
+
line: string;
|
|
397
|
+
line2: string;
|
|
398
|
+
line3: string;
|
|
399
|
+
line4: string;
|
|
400
|
+
danger: string;
|
|
401
|
+
dangerSoft: string;
|
|
402
|
+
warnSoft: string;
|
|
403
|
+
warnLine: string;
|
|
404
|
+
warnInk: string;
|
|
405
|
+
warnInk2: string;
|
|
406
|
+
successInk: string;
|
|
407
|
+
}
|
|
408
|
+
export interface CheckoutPreviewApp {
|
|
409
|
+
name: string;
|
|
410
|
+
color: string;
|
|
411
|
+
logoUrl: string | null;
|
|
412
|
+
}
|
|
413
|
+
export interface SupportContact {
|
|
414
|
+
channel: SupportChannelEnum;
|
|
415
|
+
value: string;
|
|
416
|
+
href: string;
|
|
417
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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;
|