react-native-fpay 0.4.31 → 0.4.34

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.
@@ -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: any) => r.data),
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: any) => r.data),
41
+ .then((r) => r.data),
43
42
 
44
43
  profile: () =>
45
44
  http()
46
45
  .get(`/get-user-details`)
47
- .then((r: any) => r.data.payload),
46
+ .then((r) => r.data.payload),
48
47
 
49
48
  logout: () =>
50
49
  http()
51
50
  .post('/auth/logout')
52
- .then((r: any) => r.data),
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: any) => r.data),
56
+ .then((r) => r.data),
58
57
 
59
58
  validateToken: () =>
60
- http().get<{response: HttpCallResponseFormat}>('/auth/agent/validate-token').then((r: any) => r.data),
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: boolean;
66
+ status: boolean;
66
67
  message: string;
67
- payload:any
68
- }>("shared/send-sms-otp", payload)
69
- .then((r: any)=>r.data),
70
-
71
- verifyBvn: async() =>
72
- http()
73
- .get<{response: HttpCallResponseFormat}>("verify-bvn")
74
- .then((r: any)=>r.data),
75
-
76
- verifySms: async(payload: any)=>
77
- http()
78
- .post<{response: HttpCallResponseFormat}>("verify-bvn", payload)
79
- .then((r: any)=>r.data),
80
-
81
- updateProfile: async(payload: any)=>
82
- http()
83
- .put<{response: HttpCallResponseFormat}>("update-user-agent", payload)
84
- .then((r: any)=>r.data),
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: any) => r.data.payload),
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: any) => r.data.payload),
107
+ .then((r) => r.data.payload),
107
108
 
108
109
  getAgentBalance: () =>
109
- http().get<{ payload: FPBalance }>(`/get-balance`)
110
- .then((r: any) => r.data.payload),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
142
+ .then((r) => r.data),
142
143
 
143
144
  validateTransfer: (pin: string, userId: string, receiverId: string) =>
144
145
  http()
145
- .post<{ status: boolean; message: string; payload?: { temp_id: string } }>('/validate-transaction', {
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
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: any) => r.data),
352
+ .then((r) => r.data),
256
353
 
257
354
  getDiscos: () =>
258
355
  http()
259
- .get<{ status: boolean; payload: FPBillProvider[] }>('/bills/electricity/discos')
260
- .then((r: any) => r.data),
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: any) => r.data),
367
+ .then((r) => r.data),
272
368
 
273
369
  getCableProviders: () =>
274
370
  http()
275
- .get<{ status: boolean; payload: FPBillProvider[] }>('/bills/cable/providers')
276
- .then((r: any) => r.data),
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[] }>('/bills/cable/tariffs', {
281
- params: { provider },
282
- })
283
- .then((r: any) => r.data),
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: any) => r.data),
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: any) => r.data),
400
+ .then((r) => r.data),
306
401
 
307
- purchaseAirtime: (payload: FPAirtimePurchaseRequest, tempId: string) =>
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: any) => r.data),
412
+ .then((r) => r.data),
314
413
 
315
- purchaseData: (payload: FPDataPurchaseRequest, tempId: string) =>
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: any) => r.data),
424
+ .then((r) => r.data),
322
425
 
323
- purchaseElectricity: (payload: FPElectricityPurchaseRequest, tempId: string) =>
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: any) => r.data),
436
+ .then((r) => r.data),
330
437
 
331
- purchaseCable: (payload: FPCablePurchaseRequest, tempId: string) =>
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: any) => r.data),
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
- 'Two',
204
- 'Three',
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
- return `${integer.toLocaleString()}`;
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 }> = ({