react-native-fpay 0.4.33 → 0.4.35
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/lib/module/core/api/client.js +1 -1
- package/lib/module/core/api/client.js.map +1 -1
- package/lib/module/core/api/index.js +86 -21
- package/lib/module/core/api/index.js.map +1 -1
- package/lib/module/ui/components/ConfirmScreen.js +32 -4
- package/lib/module/ui/components/ConfirmScreen.js.map +1 -1
- package/lib/module/ui/screens/BillsScreen.js +30 -14
- package/lib/module/ui/screens/BillsScreen.js.map +1 -1
- package/lib/module/ui/screens/styles.js +2 -1
- package/lib/module/ui/screens/styles.js.map +1 -1
- package/lib/module/ui/screens/sub/sendPayment/TransferSubScreen.js +10 -8
- package/lib/module/ui/screens/sub/sendPayment/TransferSubScreen.js.map +1 -1
- package/lib/typescript/src/core/api/client.d.ts.map +1 -1
- package/lib/typescript/src/core/api/index.d.ts +154 -46
- package/lib/typescript/src/core/api/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/components/ConfirmScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/BillsScreen.d.ts +1 -1
- package/lib/typescript/src/ui/screens/BillsScreen.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/styles.d.ts.map +1 -1
- package/lib/typescript/src/ui/screens/sub/sendPayment/TransferSubScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/api/client.ts +1 -2
- package/src/core/api/index.ts +197 -86
- package/src/ui/components/ConfirmScreen.tsx +37 -36
- package/src/ui/screens/BillsScreen.tsx +85 -26
- package/src/ui/screens/styles.ts +2 -1
- package/src/ui/screens/sub/sendPayment/TransferSubScreen.tsx +9 -8
package/src/core/api/index.ts
CHANGED
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
import { http } from './client';
|
|
3
3
|
import {
|
|
4
4
|
type FPBankItem,
|
|
5
|
-
type FPTransaction,
|
|
6
5
|
type FPNQRData,
|
|
7
6
|
type FPTxStatus,
|
|
8
7
|
type FPProximityPeer,
|
|
9
8
|
type FPSendPaymentRequest,
|
|
10
9
|
type FPUserInfo,
|
|
11
10
|
type FPAccount,
|
|
12
|
-
type FPSendWalletPaymentRequest,
|
|
13
11
|
type FPTransactionResponse,
|
|
14
12
|
type FPBalance,
|
|
15
13
|
type HttpCallResponseFormat,
|
|
@@ -24,6 +22,7 @@ import {
|
|
|
24
22
|
type FPDataPurchaseRequest,
|
|
25
23
|
type FPElectricityPurchaseRequest,
|
|
26
24
|
type FPCablePurchaseRequest,
|
|
25
|
+
type FPBillPurchaseRequest,
|
|
27
26
|
type FPBillTransaction,
|
|
28
27
|
BUSINESSID,
|
|
29
28
|
} from '../types';
|
|
@@ -32,56 +31,58 @@ export const healthAPI = {
|
|
|
32
31
|
ping: () =>
|
|
33
32
|
http()
|
|
34
33
|
.get('/health')
|
|
35
|
-
.then((r
|
|
34
|
+
.then((r) => r.data),
|
|
36
35
|
};
|
|
37
36
|
|
|
38
37
|
export const authenticateAPI = {
|
|
39
38
|
login: (appId: string) =>
|
|
40
39
|
http()
|
|
41
40
|
.post('/auth', { user_id: appId })
|
|
42
|
-
.then((r
|
|
41
|
+
.then((r) => r.data),
|
|
43
42
|
|
|
44
43
|
profile: () =>
|
|
45
44
|
http()
|
|
46
45
|
.get(`/get-user-details`)
|
|
47
|
-
.then((r
|
|
46
|
+
.then((r) => r.data.payload),
|
|
48
47
|
|
|
49
48
|
logout: () =>
|
|
50
49
|
http()
|
|
51
50
|
.post('/auth/logout')
|
|
52
|
-
.then((r
|
|
51
|
+
.then((r) => r.data),
|
|
53
52
|
|
|
54
53
|
validateOtp: (otp: string, email: string) =>
|
|
55
54
|
http()
|
|
56
55
|
.post<{ Response: any }>('/verify-otp', { otp, email })
|
|
57
|
-
.then((r
|
|
56
|
+
.then((r) => r.data),
|
|
58
57
|
|
|
59
58
|
validateToken: () =>
|
|
60
|
-
http()
|
|
59
|
+
http()
|
|
60
|
+
.get<{ response: HttpCallResponseFormat }>('/auth/agent/validate-token')
|
|
61
|
+
.then((r) => r.data),
|
|
61
62
|
|
|
62
|
-
sendSmsOtp: async(payload: any)=>
|
|
63
|
+
sendSmsOtp: async (payload: any) =>
|
|
63
64
|
http()
|
|
64
65
|
.post<{
|
|
65
|
-
status:
|
|
66
|
+
status: boolean;
|
|
66
67
|
message: string;
|
|
67
|
-
payload:any
|
|
68
|
-
}>(
|
|
69
|
-
.then((r
|
|
70
|
-
|
|
71
|
-
verifyBvn: async() =>
|
|
72
|
-
http()
|
|
73
|
-
.get<{response: HttpCallResponseFormat}>(
|
|
74
|
-
.then((r
|
|
75
|
-
|
|
76
|
-
verifySms: async(payload: any)=>
|
|
77
|
-
|
|
78
|
-
.post<{response: HttpCallResponseFormat}>(
|
|
79
|
-
.then((r
|
|
80
|
-
|
|
81
|
-
updateProfile: async(payload: any)=>
|
|
82
|
-
|
|
83
|
-
.put<{response: HttpCallResponseFormat}>(
|
|
84
|
-
.then((r
|
|
68
|
+
payload: any;
|
|
69
|
+
}>('shared/send-sms-otp', payload)
|
|
70
|
+
.then((r) => r.data),
|
|
71
|
+
|
|
72
|
+
verifyBvn: async () =>
|
|
73
|
+
http()
|
|
74
|
+
.get<{ response: HttpCallResponseFormat }>('verify-bvn')
|
|
75
|
+
.then((r) => r.data),
|
|
76
|
+
|
|
77
|
+
verifySms: async (payload: any) =>
|
|
78
|
+
http()
|
|
79
|
+
.post<{ response: HttpCallResponseFormat }>('verify-bvn', payload)
|
|
80
|
+
.then((r) => r.data),
|
|
81
|
+
|
|
82
|
+
updateProfile: async (payload: any) =>
|
|
83
|
+
http()
|
|
84
|
+
.put<{ response: HttpCallResponseFormat }>('update-user-agent', payload)
|
|
85
|
+
.then((r) => r.data),
|
|
85
86
|
};
|
|
86
87
|
|
|
87
88
|
export const accountAPI = {
|
|
@@ -97,25 +98,25 @@ export const accountAPI = {
|
|
|
97
98
|
nin: user.nin ?? '',
|
|
98
99
|
dateOfBirth: user.dateOfBirth ?? '',
|
|
99
100
|
})
|
|
100
|
-
.then((r
|
|
101
|
+
.then((r) => r.data.payload),
|
|
101
102
|
|
|
102
103
|
// Get account details using PSSP id
|
|
103
104
|
getAccount: () =>
|
|
104
105
|
http()
|
|
105
106
|
.get<{ payload: FPAccount }>(`/get-accounts-details`)
|
|
106
|
-
.then((r
|
|
107
|
+
.then((r) => r.data.payload),
|
|
107
108
|
|
|
108
109
|
getAgentBalance: () =>
|
|
109
|
-
http()
|
|
110
|
-
.
|
|
111
|
-
|
|
110
|
+
http()
|
|
111
|
+
.get<{ payload: FPBalance }>(`/get-balance`)
|
|
112
|
+
.then((r) => r.data.payload),
|
|
112
113
|
};
|
|
113
114
|
|
|
114
115
|
export const transferAPI = {
|
|
115
116
|
getBanks: () =>
|
|
116
117
|
http()
|
|
117
118
|
.get<FPBankItem[]>('/get-banks')
|
|
118
|
-
.then((r
|
|
119
|
+
.then((r) => r.data),
|
|
119
120
|
|
|
120
121
|
verifyAccount: (accountNumber: string, bankCode: string) =>
|
|
121
122
|
http()
|
|
@@ -128,7 +129,7 @@ export const transferAPI = {
|
|
|
128
129
|
account_no: accountNumber,
|
|
129
130
|
institution_code: bankCode,
|
|
130
131
|
})
|
|
131
|
-
.then((r
|
|
132
|
+
.then((r) => r.data),
|
|
132
133
|
|
|
133
134
|
verifyWalletAccount: (accountNumber: string) =>
|
|
134
135
|
http()
|
|
@@ -138,16 +139,20 @@ export const transferAPI = {
|
|
|
138
139
|
bankName: string;
|
|
139
140
|
bankCode: string;
|
|
140
141
|
}>('/wallet-name-enquiry', { account_number: accountNumber })
|
|
141
|
-
.then((r
|
|
142
|
+
.then((r) => r.data),
|
|
142
143
|
|
|
143
144
|
validateTransfer: (pin: string, userId: string, receiverId: string) =>
|
|
144
145
|
http()
|
|
145
|
-
.post<{
|
|
146
|
+
.post<{
|
|
147
|
+
status: boolean;
|
|
148
|
+
message: string;
|
|
149
|
+
payload?: { temp_id: string };
|
|
150
|
+
}>('/validate-transaction', {
|
|
146
151
|
pin,
|
|
147
152
|
sender_type: 'AGENT',
|
|
148
153
|
receiver_id: receiverId,
|
|
149
154
|
})
|
|
150
|
-
.then((r
|
|
155
|
+
.then((r) => r.data),
|
|
151
156
|
|
|
152
157
|
sendToWallet: (payload: any, temptId: string) =>
|
|
153
158
|
http()
|
|
@@ -155,7 +160,7 @@ export const transferAPI = {
|
|
|
155
160
|
...payload,
|
|
156
161
|
temp_id: temptId,
|
|
157
162
|
})
|
|
158
|
-
.then((r
|
|
163
|
+
.then((r) => r.data),
|
|
159
164
|
|
|
160
165
|
// Send to external bank account
|
|
161
166
|
sendToBank: (payload: FPSendPaymentRequest, temptId: string) =>
|
|
@@ -164,19 +169,19 @@ export const transferAPI = {
|
|
|
164
169
|
...payload,
|
|
165
170
|
temp_id: temptId,
|
|
166
171
|
})
|
|
167
|
-
.then((r
|
|
172
|
+
.then((r) => r.data),
|
|
168
173
|
|
|
169
174
|
status: (reference: string) =>
|
|
170
175
|
http()
|
|
171
176
|
.get<{ status: FPTxStatus; reference: string }>(
|
|
172
177
|
'/get-transaction-status/' + reference
|
|
173
178
|
)
|
|
174
|
-
.then((r
|
|
179
|
+
.then((r) => r.data),
|
|
175
180
|
|
|
176
181
|
verify: (reference: string) =>
|
|
177
182
|
http()
|
|
178
183
|
.get<{ FPTransactionResponse: any }>('/transaction/verify/' + reference)
|
|
179
|
-
.then((r
|
|
184
|
+
.then((r) => r.data),
|
|
180
185
|
};
|
|
181
186
|
|
|
182
187
|
export const nqrAPI = {
|
|
@@ -188,19 +193,19 @@ export const nqrAPI = {
|
|
|
188
193
|
}) =>
|
|
189
194
|
http()
|
|
190
195
|
.post<FPNQRData>('/generate-nqr', payload)
|
|
191
|
-
.then((r
|
|
196
|
+
.then((r) => r.data),
|
|
192
197
|
|
|
193
198
|
pay: (payload: FPSendPaymentRequest, temptId: string) =>
|
|
194
199
|
http()
|
|
195
200
|
.post<FPTransactionResponse>('/pay-nqr', { ...payload, temp_id: temptId })
|
|
196
|
-
.then((r
|
|
201
|
+
.then((r) => r.data),
|
|
197
202
|
};
|
|
198
203
|
|
|
199
204
|
export const nfcAPI = {
|
|
200
205
|
pay: (payload: FPSendPaymentRequest, temptId: string) =>
|
|
201
206
|
http()
|
|
202
207
|
.post<FPTransactionResponse>('/pay-nfc', { ...payload, temp_id: temptId })
|
|
203
|
-
.then((r
|
|
208
|
+
.then((r) => r.data),
|
|
204
209
|
};
|
|
205
210
|
|
|
206
211
|
export const proximityAPI = {
|
|
@@ -210,7 +215,7 @@ export const proximityAPI = {
|
|
|
210
215
|
) =>
|
|
211
216
|
http()
|
|
212
217
|
.post<{ sessionId: string }>(`/broadcast-proximity/${psspId}`, payload)
|
|
213
|
-
.then((r
|
|
218
|
+
.then((r) => r.data),
|
|
214
219
|
|
|
215
220
|
heartbeat: (sessionId: string, lat: number, lng: number) =>
|
|
216
221
|
http().patch(`/broadcast-proximity/${sessionId}`, {
|
|
@@ -228,113 +233,219 @@ export const proximityAPI = {
|
|
|
228
233
|
longitude: lng,
|
|
229
234
|
radius_meters,
|
|
230
235
|
})
|
|
231
|
-
.then((r
|
|
236
|
+
.then((r) => r.data),
|
|
232
237
|
};
|
|
233
238
|
|
|
234
|
-
|
|
235
239
|
export const subscriptionAPI = {
|
|
236
|
-
create: (
|
|
237
|
-
payload: any
|
|
238
|
-
) =>
|
|
240
|
+
create: (payload: any) =>
|
|
239
241
|
http()
|
|
240
242
|
.post<{ sessionId: string }>(`/subscription/`, payload)
|
|
241
|
-
.then((r
|
|
243
|
+
.then((r) => r.data),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/** Context every bill purchase needs beyond the category-specific fields —
|
|
247
|
+
* mirrors what SendScreen already assembles for transfers (agent identity,
|
|
248
|
+
* terminal, geo-location for terminal transactions, idempotency reference).
|
|
249
|
+
* Built once by BillsScreen and passed to whichever purchaseX() applies. */
|
|
250
|
+
export interface FPBillPurchaseContext {
|
|
251
|
+
agentId: string; // getFPStore().psspId
|
|
252
|
+
terminalId?: string;
|
|
253
|
+
/** Only present for terminal/agent transactions — mirrors SendScreen's
|
|
254
|
+
* requestLocation() + isTerminalTransaction gating exactly. */
|
|
255
|
+
geoLocation?: { latitude: number; longitude: number };
|
|
256
|
+
currency: string;
|
|
257
|
+
tnxRef: string;
|
|
258
|
+
paymentType?: string; // defaults to "ONLINE" to match Tapit's existing screens
|
|
259
|
+
/** Self-service vs. third-party recipient — only meaningful for
|
|
260
|
+
* ELECTRICITY today, but accepted generically since BuypowerPurchaseView
|
|
261
|
+
* branches on `for_self` for the name/email/phone substitution. */
|
|
262
|
+
forSelf?: boolean;
|
|
263
|
+
agentName?: string;
|
|
264
|
+
agentEmail?: string;
|
|
265
|
+
agentPhone?: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Maps this SDK's clean, kobo-based internal request shape onto the real
|
|
269
|
+
* wire format BuypowerPurchaseView (Django) expects. Kept as one function
|
|
270
|
+
* so every purchase call builds the body identically — if the backend
|
|
271
|
+
* contract changes, this is the one place to update. */
|
|
272
|
+
function toBillWirePayload(
|
|
273
|
+
payload: FPBillPurchaseRequest,
|
|
274
|
+
ctx: FPBillPurchaseContext
|
|
275
|
+
): Record<string, unknown> {
|
|
276
|
+
const base: Record<string, unknown> = {
|
|
277
|
+
agent_id: ctx.agentId,
|
|
278
|
+
amount: (payload.amountInKobo / 100).toFixed(2),
|
|
279
|
+
channel: payload.category,
|
|
280
|
+
terminal_id: ctx.terminalId,
|
|
281
|
+
tnx_ref: ctx.tnxRef,
|
|
282
|
+
currency: ctx.currency,
|
|
283
|
+
payment_type: ctx.paymentType ?? 'ONLINE',
|
|
284
|
+
geo_location: ctx.geoLocation,
|
|
285
|
+
for_self: ctx.forSelf ?? true,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
switch (payload.category) {
|
|
289
|
+
case 'AIRTIME':
|
|
290
|
+
return {
|
|
291
|
+
...base,
|
|
292
|
+
meter: payload.phoneNumber, // BuypowerPurchaseView reuses `meter` as the recipient identifier across categories
|
|
293
|
+
disco: payload.network,
|
|
294
|
+
phone: ctx.agentPhone,
|
|
295
|
+
name: ctx.agentName,
|
|
296
|
+
email: ctx.agentEmail,
|
|
297
|
+
};
|
|
298
|
+
case 'DATA':
|
|
299
|
+
return {
|
|
300
|
+
...base,
|
|
301
|
+
meter: payload.phoneNumber,
|
|
302
|
+
disco: payload.network,
|
|
303
|
+
tariff_class: payload.planId,
|
|
304
|
+
phone: ctx.agentPhone,
|
|
305
|
+
name: ctx.agentName,
|
|
306
|
+
email: ctx.agentEmail,
|
|
307
|
+
};
|
|
308
|
+
case 'ELECTRICITY':
|
|
309
|
+
return {
|
|
310
|
+
...base,
|
|
311
|
+
meter: payload.meterNumber,
|
|
312
|
+
disco: payload.disco,
|
|
313
|
+
vend_type: payload.meterType,
|
|
314
|
+
for_self: payload.forSelf,
|
|
315
|
+
phone: payload.forSelf ? ctx.agentPhone : payload.phoneNumber,
|
|
316
|
+
email: payload.forSelf ? ctx.agentEmail : payload.email,
|
|
317
|
+
name: ctx.agentName,
|
|
318
|
+
};
|
|
319
|
+
case 'CABLE':
|
|
320
|
+
return {
|
|
321
|
+
...base,
|
|
322
|
+
meter: payload.smartcardNumber,
|
|
323
|
+
disco: payload.provider,
|
|
324
|
+
tariff_class: payload.tariffCode,
|
|
325
|
+
phone: payload.phoneNumber,
|
|
326
|
+
name: ctx.agentName,
|
|
327
|
+
email: ctx.agentEmail,
|
|
328
|
+
};
|
|
329
|
+
default: {
|
|
330
|
+
// Exhaustiveness check — if a fifth category is ever added to
|
|
331
|
+
// FPBillPurchaseRequest without a case here, this fails to compile
|
|
332
|
+
// instead of silently sending an unmapped payload to the backend.
|
|
333
|
+
const _exhaustive: never = payload;
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Unhandled bill category: ${JSON.stringify(_exhaustive)}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
242
339
|
}
|
|
243
340
|
|
|
244
341
|
export const billsAPI = {
|
|
245
342
|
getNetworks: () =>
|
|
246
343
|
http()
|
|
247
344
|
.get<{ status: boolean; payload: FPNetworkOperator[] }>('/bills/networks')
|
|
248
|
-
.then((r
|
|
345
|
+
.then((r) => r.data),
|
|
249
346
|
|
|
250
347
|
getDataPlans: (network: FPNetworkCode) =>
|
|
251
348
|
http()
|
|
252
349
|
.get<{ status: boolean; payload: FPDataPlan[] }>('/bills/data-plans', {
|
|
253
350
|
params: { network },
|
|
254
351
|
})
|
|
255
|
-
.then((r
|
|
352
|
+
.then((r) => r.data),
|
|
256
353
|
|
|
257
354
|
getDiscos: () =>
|
|
258
355
|
http()
|
|
259
|
-
.get<{ status: boolean; payload: FPBillProvider[] }>(
|
|
260
|
-
|
|
356
|
+
.get<{ status: boolean; payload: FPBillProvider[] }>(
|
|
357
|
+
'/bills/electricity/discos'
|
|
358
|
+
)
|
|
359
|
+
.then((r) => r.data),
|
|
261
360
|
|
|
262
|
-
/** Validates a meter number against a disco before purchase — mirrors
|
|
263
|
-
* the existing CheckMeterNo() lookup in Tapit's BuyPower-backed
|
|
264
|
-
* Electricity screen, generalized behind this SDK's own endpoint. */
|
|
265
361
|
validateMeter: (meterNumber: string, disco: string, meterType: FPMeterType) =>
|
|
266
362
|
http()
|
|
267
363
|
.post<{ status: boolean; message: string; payload: FPMeterLookupResult }>(
|
|
268
364
|
'/bills/electricity/validate-meter',
|
|
269
365
|
{ meter_number: meterNumber, disco, meter_type: meterType }
|
|
270
366
|
)
|
|
271
|
-
.then((r
|
|
367
|
+
.then((r) => r.data),
|
|
272
368
|
|
|
273
369
|
getCableProviders: () =>
|
|
274
370
|
http()
|
|
275
|
-
.get<{ status: boolean; payload: FPBillProvider[] }>(
|
|
276
|
-
|
|
371
|
+
.get<{ status: boolean; payload: FPBillProvider[] }>(
|
|
372
|
+
'/bills/cable/providers'
|
|
373
|
+
)
|
|
374
|
+
.then((r) => r.data),
|
|
277
375
|
|
|
278
376
|
getCableTariffs: (provider: string) =>
|
|
279
377
|
http()
|
|
280
|
-
.get<{ status: boolean; payload: FPBillTariff[] }>(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
378
|
+
.get<{ status: boolean; payload: FPBillTariff[] }>(
|
|
379
|
+
'/bills/cable/tariffs',
|
|
380
|
+
{
|
|
381
|
+
params: { provider },
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
.then((r) => r.data),
|
|
284
385
|
|
|
285
|
-
/** Validates a smartcard/IUC number before purchase. */
|
|
286
386
|
validateSmartcard: (smartcardNumber: string, provider: string) =>
|
|
287
387
|
http()
|
|
288
388
|
.post<{ status: boolean; message: string; payload: FPMeterLookupResult }>(
|
|
289
389
|
'/bills/cable/validate-smartcard',
|
|
290
390
|
{ smartcard_number: smartcardNumber, provider }
|
|
291
391
|
)
|
|
292
|
-
.then((r
|
|
392
|
+
.then((r) => r.data),
|
|
293
393
|
|
|
294
|
-
/** Separate from transferAPI.validateTransfer — bills have no transfer
|
|
295
|
-
* recipient, so this hits its own backend endpoint rather than reusing
|
|
296
|
-
* /validate-transaction's receiver_id-shaped contract. One endpoint
|
|
297
|
-
* covers all four categories — the backend doesn't need to know which
|
|
298
|
-
* category is being authorized at PIN-check time. */
|
|
299
394
|
validateBillPin: (pin: string, userId: string) =>
|
|
300
395
|
http()
|
|
301
396
|
.post<{ status: boolean; message: string; payload: { temp_id: string } }>(
|
|
302
397
|
'/bills/validate-pin',
|
|
303
398
|
{ pin, user_id: userId }
|
|
304
399
|
)
|
|
305
|
-
.then((r
|
|
400
|
+
.then((r) => r.data),
|
|
306
401
|
|
|
307
|
-
purchaseAirtime: (
|
|
402
|
+
purchaseAirtime: (
|
|
403
|
+
payload: FPAirtimePurchaseRequest,
|
|
404
|
+
ctx: FPBillPurchaseContext,
|
|
405
|
+
tempId: string
|
|
406
|
+
) =>
|
|
308
407
|
http()
|
|
309
408
|
.post<{ status: boolean; message: string; payload: FPBillTransaction }>(
|
|
310
409
|
'/bills/airtime',
|
|
311
|
-
{ ...payload, temp_id: tempId }
|
|
410
|
+
{ ...toBillWirePayload(payload, ctx), temp_id: tempId }
|
|
312
411
|
)
|
|
313
|
-
.then((r
|
|
412
|
+
.then((r) => r.data),
|
|
314
413
|
|
|
315
|
-
purchaseData: (
|
|
414
|
+
purchaseData: (
|
|
415
|
+
payload: FPDataPurchaseRequest,
|
|
416
|
+
ctx: FPBillPurchaseContext,
|
|
417
|
+
tempId: string
|
|
418
|
+
) =>
|
|
316
419
|
http()
|
|
317
420
|
.post<{ status: boolean; message: string; payload: FPBillTransaction }>(
|
|
318
421
|
'/bills/data',
|
|
319
|
-
{ ...payload, temp_id: tempId }
|
|
422
|
+
{ ...toBillWirePayload(payload, ctx), temp_id: tempId }
|
|
320
423
|
)
|
|
321
|
-
.then((r
|
|
424
|
+
.then((r) => r.data),
|
|
322
425
|
|
|
323
|
-
purchaseElectricity: (
|
|
426
|
+
purchaseElectricity: (
|
|
427
|
+
payload: FPElectricityPurchaseRequest,
|
|
428
|
+
ctx: FPBillPurchaseContext,
|
|
429
|
+
tempId: string
|
|
430
|
+
) =>
|
|
324
431
|
http()
|
|
325
432
|
.post<{ status: boolean; message: string; payload: FPBillTransaction }>(
|
|
326
433
|
'/bills/electricity',
|
|
327
|
-
{ ...payload, temp_id: tempId }
|
|
434
|
+
{ ...toBillWirePayload(payload, ctx), temp_id: tempId }
|
|
328
435
|
)
|
|
329
|
-
.then((r
|
|
436
|
+
.then((r) => r.data),
|
|
330
437
|
|
|
331
|
-
purchaseCable: (
|
|
438
|
+
purchaseCable: (
|
|
439
|
+
payload: FPCablePurchaseRequest,
|
|
440
|
+
ctx: FPBillPurchaseContext,
|
|
441
|
+
tempId: string
|
|
442
|
+
) =>
|
|
332
443
|
http()
|
|
333
444
|
.post<{ status: boolean; message: string; payload: FPBillTransaction }>(
|
|
334
445
|
'/bills/cable',
|
|
335
|
-
{ ...payload, temp_id: tempId }
|
|
446
|
+
{ ...toBillWirePayload(payload, ctx), temp_id: tempId }
|
|
336
447
|
)
|
|
337
|
-
.then((r
|
|
448
|
+
.then((r) => r.data),
|
|
338
449
|
|
|
339
450
|
/** Bill transactions live in the same agencyTransaction table as
|
|
340
451
|
* transfers, so the existing status endpoint is reused as-is. */
|
|
@@ -199,49 +199,50 @@ interface ConfirmScreenProps {
|
|
|
199
199
|
|
|
200
200
|
const getAmountInWords = (amount: number): string => {
|
|
201
201
|
const ones = [
|
|
202
|
-
'One',
|
|
203
|
-
'
|
|
204
|
-
'
|
|
205
|
-
'Four',
|
|
206
|
-
'Five',
|
|
207
|
-
'Six',
|
|
208
|
-
'Seven',
|
|
209
|
-
'Eight',
|
|
210
|
-
'Nine',
|
|
211
|
-
'Ten',
|
|
212
|
-
'Eleven',
|
|
213
|
-
'Twelve',
|
|
214
|
-
'Thirteen',
|
|
215
|
-
'Fourteen',
|
|
216
|
-
'Fifteen',
|
|
217
|
-
'Sixteen',
|
|
218
|
-
'Seventeen',
|
|
219
|
-
'Eighteen',
|
|
220
|
-
'Nineteen',
|
|
202
|
+
'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine',
|
|
203
|
+
'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen',
|
|
204
|
+
'Seventeen', 'Eighteen', 'Nineteen',
|
|
221
205
|
];
|
|
222
206
|
const tens = [
|
|
223
|
-
'',
|
|
224
|
-
'',
|
|
225
|
-
'Twenty',
|
|
226
|
-
'Thirty',
|
|
227
|
-
'Forty',
|
|
228
|
-
'Fifty',
|
|
229
|
-
'Sixty',
|
|
230
|
-
'Seventy',
|
|
231
|
-
'Eighty',
|
|
232
|
-
'Ninety',
|
|
207
|
+
'', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety',
|
|
233
208
|
];
|
|
209
|
+
// index 0 = the base 0-999 group (no suffix), then Thousand, Million, etc.
|
|
210
|
+
const scale = ['', 'Thousand', 'Million', 'Billion', 'Trillion'];
|
|
211
|
+
|
|
212
|
+
// Converts a 0-999 chunk to words — no recursion past 3 digits.
|
|
213
|
+
const chunkToWords = (n: number): string => {
|
|
214
|
+
if (n === 0) return '';
|
|
215
|
+
if (n < 20) return ones[n - 1] as string;
|
|
216
|
+
if (n < 100) {
|
|
217
|
+
const t: string = tens[Math.floor(n / 10)] || '';
|
|
218
|
+
const o: number = n % 10;
|
|
219
|
+
return o ? `${t} ${ones[o - 1]}` : t;
|
|
220
|
+
}
|
|
221
|
+
const hundredsDigit = Math.floor(n / 100);
|
|
222
|
+
const remainder = n % 100;
|
|
223
|
+
const hundredsPart = `${ones[hundredsDigit - 1]} Hundred`;
|
|
224
|
+
return remainder ? `${hundredsPart} ${chunkToWords(remainder)}` : hundredsPart;
|
|
225
|
+
};
|
|
234
226
|
|
|
235
227
|
const integer = Math.floor(amount);
|
|
236
|
-
|
|
237
228
|
if (integer === 0) return 'Zero';
|
|
238
|
-
if (integer < 20) return ones[integer] || '';
|
|
239
|
-
if (integer < 100)
|
|
240
|
-
return `${tens[Math.floor(integer / 10)]}${integer % 10 ? ' ' + ones[integer % 10] : ''}`;
|
|
241
|
-
if (integer < 1000)
|
|
242
|
-
return `${ones[Math.floor(integer / 100)]} Hundred${integer % 100 ? ' ' + getAmountInWords(integer % 100) : ''}`;
|
|
243
229
|
|
|
244
|
-
|
|
230
|
+
// Break into groups of 3 digits, smallest group first.
|
|
231
|
+
const groups: number[] = [];
|
|
232
|
+
let n = integer;
|
|
233
|
+
while (n > 0) {
|
|
234
|
+
groups.push(n % 1000);
|
|
235
|
+
n = Math.floor(n / 1000);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const parts: string[] = [];
|
|
239
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
240
|
+
if (groups[i] === 0) continue;
|
|
241
|
+
const words = chunkToWords(groups[i] as number);
|
|
242
|
+
parts.push(scale[i] ? `${words} ${scale[i]}` : words);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return parts.join(' ');
|
|
245
246
|
};
|
|
246
247
|
// ─── Sub-components ──────────────────────────────────────────────────────────
|
|
247
248
|
const DetailRow: React.FC<{ label: string; value: string }> = ({
|