@nile-squad/nylonpay-ts 1.0.10 → 1.2.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/dist/index.cjs CHANGED
@@ -255,13 +255,13 @@ function createTransport({
255
255
  async function send(request) {
256
256
  const envelope = buildEnvelope(request);
257
257
  const signedPayload = envelope.payload;
258
- const headers = buildAuthHeaders({
259
- apiKey,
260
- apiSecret,
261
- payload: signedPayload
262
- });
263
258
  const bodyString = JSON.stringify(envelope);
264
259
  async function attempt(currentAttempt) {
260
+ const headers = buildAuthHeaders({
261
+ apiKey,
262
+ apiSecret,
263
+ payload: signedPayload
264
+ });
265
265
  const { controller, cleanup } = withTimeout(timeoutMs);
266
266
  try {
267
267
  const response = await fetchImpl(baseUrl, {
@@ -300,7 +300,7 @@ function createTransport({
300
300
  return slangTs.Err(
301
301
  JSON.stringify({
302
302
  category: "internal",
303
- message: "Response missing status field",
303
+ message: "Received an invalid response from the server",
304
304
  retryable: false
305
305
  })
306
306
  );
@@ -313,7 +313,7 @@ function createTransport({
313
313
  return slangTs.Err(
314
314
  JSON.stringify({
315
315
  category: "internal",
316
- message: "Response signature missing",
316
+ message: "Could not verify the server response",
317
317
  retryable: false
318
318
  })
319
319
  );
@@ -328,7 +328,7 @@ function createTransport({
328
328
  return slangTs.Err(
329
329
  JSON.stringify({
330
330
  category: "internal",
331
- message: "Response signature verification failed",
331
+ message: "Could not verify the server response",
332
332
  retryable: false
333
333
  })
334
334
  );
@@ -344,7 +344,7 @@ function createTransport({
344
344
  const isAbort = error instanceof DOMException && error.name === "AbortError";
345
345
  const sdkError = {
346
346
  category: isAbort ? "timeout" : "network",
347
- message: isAbort ? `Request timed out after ${timeoutMs}ms` : String(error),
347
+ message: isAbort ? "The request timed out" : "Could not reach the server, check your network connection and try again",
348
348
  retryable: true
349
349
  };
350
350
  if (currentAttempt < maxRetries) {
@@ -375,6 +375,7 @@ function parseError(error) {
375
375
 
376
376
  // src/payment.ts
377
377
  var STATUS_TO_EVENT = {
378
+ pending: "processing",
378
379
  successful: "success",
379
380
  failed: "failed",
380
381
  processing: "processing",
@@ -398,6 +399,7 @@ function createPaymentInstance(initialResponse, deps) {
398
399
  status: normalizeStatus(initialResponse.status),
399
400
  transaction: null,
400
401
  pollingTimer: null,
402
+ lastStatusEvent: null,
401
403
  resolved: false,
402
404
  pollAttempts: 0,
403
405
  pollStartTime: Date.now(),
@@ -408,14 +410,21 @@ function createPaymentInstance(initialResponse, deps) {
408
410
  maxPollDuration: deps.maxPollDuration ?? 3e5,
409
411
  maxPollAttempts: deps.maxPollAttempts ?? 150
410
412
  };
411
- function resolveWithError(error) {
413
+ function resolveWithError(error, category, retryable) {
412
414
  state.resolved = true;
413
415
  stopUpdates();
414
- emitEvent("error", parseError(error).message);
416
+ const parsed = parseError(error);
417
+ emitEvent(
418
+ "error",
419
+ parsed.message,
420
+ category ?? parsed.category,
421
+ parsed.retryable
422
+ );
415
423
  }
416
424
  function emitEvent(event, error, category, retryable) {
417
425
  const data = {
418
426
  event,
427
+ reference: state.reference,
419
428
  transaction: state.transaction ?? void 0,
420
429
  error,
421
430
  category,
@@ -447,30 +456,30 @@ function createPaymentInstance(initialResponse, deps) {
447
456
  }
448
457
  if (response.reference !== state.reference) {
449
458
  resolveWithError(
450
- `Reference mismatch: expected ${state.reference} but got ${response.reference}`
459
+ "Received a status update for a different transaction",
460
+ "internal"
451
461
  );
452
462
  return;
453
463
  }
454
464
  const newStatus = normalizeStatus(response.status);
455
- const oldStatus = state.status;
456
465
  state.status = newStatus;
457
- if (newStatus !== oldStatus) {
458
- const event = statusToEvent(newStatus);
459
- if (event) {
460
- if (TERMINAL_STATES.has(newStatus)) {
461
- await handleTerminalState(newStatus);
462
- return;
463
- }
464
- emitEvent(event);
465
- }
466
+ const event = statusToEvent(newStatus);
467
+ if (!event || event === state.lastStatusEvent) {
468
+ return;
466
469
  }
470
+ state.lastStatusEvent = event;
471
+ if (TERMINAL_STATES.has(newStatus)) {
472
+ await handleTerminalState(newStatus);
473
+ return;
474
+ }
475
+ emitEvent(event);
467
476
  }
468
477
  function handlePollError(error) {
469
478
  const parsed = parseError(error);
470
479
  if (parsed.category === "not_found") {
471
480
  return;
472
481
  }
473
- emitEvent("error", parsed.message);
482
+ emitEvent("error", parsed.message, parsed.category, parsed.retryable);
474
483
  state.resolved = true;
475
484
  stopUpdates();
476
485
  }
@@ -490,11 +499,17 @@ function createPaymentInstance(initialResponse, deps) {
490
499
  return;
491
500
  }
492
501
  if (state.pollAttempts >= state.maxPollAttempts) {
493
- resolveWithError("Polling timeout: exceeded maximum attempts");
502
+ resolveWithError(
503
+ "Timed out waiting for the transaction status to update",
504
+ "timeout"
505
+ );
494
506
  return;
495
507
  }
496
508
  if (Date.now() - state.pollStartTime >= state.maxPollDuration) {
497
- resolveWithError("Polling timeout: exceeded maximum duration");
509
+ resolveWithError(
510
+ "Timed out waiting for the transaction status to update",
511
+ "timeout"
512
+ );
498
513
  return;
499
514
  }
500
515
  state.pollAttempts += 1;
@@ -517,6 +532,15 @@ function createPaymentInstance(initialResponse, deps) {
517
532
  }, 0);
518
533
  return;
519
534
  }
535
+ const initialEvent = statusToEvent(state.status);
536
+ if (initialEvent) {
537
+ state.lastStatusEvent = initialEvent;
538
+ setTimeout(() => {
539
+ if (!state.resolved) {
540
+ emitEvent(initialEvent);
541
+ }
542
+ }, 0);
543
+ }
520
544
  scheduleNextPoll();
521
545
  }
522
546
  function stopUpdates() {
@@ -605,6 +629,9 @@ function normalizePhone(phone) {
605
629
  }
606
630
  return normalized;
607
631
  }
632
+ function isValidPhoneFormat(normalizedPhone) {
633
+ return /^\d{9,15}$/.test(normalizedPhone);
634
+ }
608
635
  var DEFAULT_TOLERANCE_SECONDS = 300;
609
636
  function decodePayload(payload) {
610
637
  return typeof payload === "string" ? payload : Buffer.from(payload).toString("utf8");
@@ -701,6 +728,56 @@ function validateNonEmpty(value, fieldName) {
701
728
  throwValidation(`${fieldName} is required`);
702
729
  }
703
730
  }
731
+ function validatePhoneFormat(normalizedPhone, fieldName) {
732
+ if (!isValidPhoneFormat(normalizedPhone)) {
733
+ throwValidation(`${fieldName} must be a valid phone number`);
734
+ }
735
+ }
736
+ function prepareCollectPayload(input) {
737
+ const reference = resolveReference(input.reference);
738
+ validateCollectionAmount(input.amount);
739
+ validateNonEmpty(input.customer.name, "customer.name");
740
+ validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
741
+ const normalizedPhone = normalizePhone(input.customer.phoneNumber);
742
+ validatePhoneFormat(normalizedPhone, "customer.phoneNumber");
743
+ validateNonEmpty(input.description, "description");
744
+ if (input.method === "bank" && !input.bank) {
745
+ throwValidation('bank details are required when method is "bank"');
746
+ }
747
+ return {
748
+ ...input,
749
+ reference,
750
+ customer: { ...input.customer, phoneNumber: normalizedPhone }
751
+ };
752
+ }
753
+ function preparePayoutPayload(input) {
754
+ const reference = resolveReference(input.reference);
755
+ validatePayoutAmount(input.amount);
756
+ validateNonEmpty(input.customer.name, "customer.name");
757
+ validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
758
+ const normalizedPhone = normalizePhone(input.customer.phoneNumber);
759
+ validatePhoneFormat(normalizedPhone, "customer.phoneNumber");
760
+ validateNonEmpty(input.description, "description");
761
+ validateNonEmpty(
762
+ input.destination.accountHolderName,
763
+ "destination.accountHolderName"
764
+ );
765
+ validateNonEmpty(
766
+ input.destination.accountNumber,
767
+ "destination.accountNumber"
768
+ );
769
+ return {
770
+ ...input,
771
+ reference,
772
+ customer: { ...input.customer, phoneNumber: normalizedPhone }
773
+ };
774
+ }
775
+ function applyBeforeHookMutation(mutated, current, prepare) {
776
+ return prepare({
777
+ ...mutated,
778
+ reference: mutated.reference ?? current.reference
779
+ });
780
+ }
704
781
  function createSdkInstance(config) {
705
782
  const transport = createTransport({
706
783
  apiKey: config.apiKey,
@@ -724,23 +801,15 @@ function createSdkInstance(config) {
724
801
  maxPollAttempts: config.maxPollAttempts
725
802
  };
726
803
  async function collectPayment(input) {
727
- const reference = resolveReference(input.reference);
728
- validateCollectionAmount(input.amount);
729
- validateNonEmpty(input.customer.name, "customer.name");
730
- validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
731
- const normalizedPhone = normalizePhone(input.customer.phoneNumber);
732
- validateNonEmpty(input.description, "description");
733
- if (input.method === "bank" && !input.bank) {
734
- throwValidation('bank details are required when method is "bank"');
735
- }
736
- let payload = {
737
- ...input,
738
- reference,
739
- customer: { ...input.customer, phoneNumber: normalizedPhone }
740
- };
804
+ let payload = prepareCollectPayload(input);
741
805
  const mutated = await runHook(config.hooks?.beforeCollect, payload);
742
- if (mutated != null)
743
- payload = { ...mutated, reference: mutated.reference ?? reference };
806
+ if (mutated != null) {
807
+ payload = applyBeforeHookMutation(
808
+ mutated,
809
+ payload,
810
+ prepareCollectPayload
811
+ );
812
+ }
744
813
  const result = await transport.send({
745
814
  action: SDK_ACTIONS.collectPayment,
746
815
  payload
@@ -748,35 +817,27 @@ function createSdkInstance(config) {
748
817
  await runHook(
749
818
  config.hooks?.afterCollect,
750
819
  result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
751
- payload
820
+ { ...payload, raw: input }
752
821
  );
753
822
  if (result.isErr) {
754
823
  const sdkErr = parseError(result.error);
755
824
  return createPaymentInstance(
756
- { reference, status: "pending" },
825
+ { reference: payload.reference, status: "pending" },
757
826
  { ...commonDeps, initialError: sdkErr }
758
827
  );
759
828
  }
760
829
  return createPaymentInstance(result.value, commonDeps);
761
830
  }
762
831
  async function collectPaymentAndResolve(input) {
763
- const reference = resolveReference(input.reference);
764
- validateCollectionAmount(input.amount);
765
- validateNonEmpty(input.customer.name, "customer.name");
766
- validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
767
- const normalizedPhone = normalizePhone(input.customer.phoneNumber);
768
- validateNonEmpty(input.description, "description");
769
- if (input.method === "bank" && !input.bank) {
770
- throwValidation('bank details are required when method is "bank"');
771
- }
772
- let payload = {
773
- ...input,
774
- reference,
775
- customer: { ...input.customer, phoneNumber: normalizedPhone }
776
- };
832
+ let payload = prepareCollectPayload(input);
777
833
  const mutated = await runHook(config.hooks?.beforeCollect, payload);
778
- if (mutated != null)
779
- payload = { ...mutated, reference: mutated.reference ?? reference };
834
+ if (mutated != null) {
835
+ payload = applyBeforeHookMutation(
836
+ mutated,
837
+ payload,
838
+ prepareCollectPayload
839
+ );
840
+ }
780
841
  const result = await transport.send({
781
842
  action: SDK_ACTIONS.collectPaymentAndResolve,
782
843
  payload
@@ -784,7 +845,7 @@ function createSdkInstance(config) {
784
845
  await runHook(
785
846
  config.hooks?.afterCollect,
786
847
  result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
787
- payload
848
+ { ...payload, raw: input }
788
849
  );
789
850
  if (result.isOk) {
790
851
  return slangTs.Ok(result.value);
@@ -792,28 +853,11 @@ function createSdkInstance(config) {
792
853
  return slangTs.Err(result.error);
793
854
  }
794
855
  async function makePayout(input) {
795
- const reference = resolveReference(input.reference);
796
- validatePayoutAmount(input.amount);
797
- validateNonEmpty(input.customer.name, "customer.name");
798
- validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
799
- const normalizedPhone = normalizePhone(input.customer.phoneNumber);
800
- validateNonEmpty(input.description, "description");
801
- validateNonEmpty(
802
- input.destination.accountHolderName,
803
- "destination.accountHolderName"
804
- );
805
- validateNonEmpty(
806
- input.destination.accountNumber,
807
- "destination.accountNumber"
808
- );
809
- let payload = {
810
- ...input,
811
- reference,
812
- customer: { ...input.customer, phoneNumber: normalizedPhone }
813
- };
856
+ let payload = preparePayoutPayload(input);
814
857
  const mutated = await runHook(config.hooks?.beforePayout, payload);
815
- if (mutated != null)
816
- payload = { ...mutated, reference: mutated.reference ?? reference };
858
+ if (mutated != null) {
859
+ payload = applyBeforeHookMutation(mutated, payload, preparePayoutPayload);
860
+ }
817
861
  const result = await transport.send({
818
862
  action: SDK_ACTIONS.makePayout,
819
863
  payload
@@ -821,40 +865,23 @@ function createSdkInstance(config) {
821
865
  await runHook(
822
866
  config.hooks?.afterPayout,
823
867
  result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
824
- payload
868
+ { ...payload, raw: input }
825
869
  );
826
870
  if (result.isErr) {
827
871
  const sdkErr = parseError(result.error);
828
872
  return createPaymentInstance(
829
- { reference, status: "pending" },
873
+ { reference: payload.reference, status: "pending" },
830
874
  { ...commonDeps, initialError: sdkErr }
831
875
  );
832
876
  }
833
877
  return createPaymentInstance(result.value, commonDeps);
834
878
  }
835
879
  async function makePayoutAndResolve(input) {
836
- const reference = resolveReference(input.reference);
837
- validatePayoutAmount(input.amount);
838
- validateNonEmpty(input.customer.name, "customer.name");
839
- validateNonEmpty(input.customer.phoneNumber, "customer.phoneNumber");
840
- const normalizedPhone = normalizePhone(input.customer.phoneNumber);
841
- validateNonEmpty(input.description, "description");
842
- validateNonEmpty(
843
- input.destination.accountHolderName,
844
- "destination.accountHolderName"
845
- );
846
- validateNonEmpty(
847
- input.destination.accountNumber,
848
- "destination.accountNumber"
849
- );
850
- let payload = {
851
- ...input,
852
- reference,
853
- customer: { ...input.customer, phoneNumber: normalizedPhone }
854
- };
880
+ let payload = preparePayoutPayload(input);
855
881
  const mutated = await runHook(config.hooks?.beforePayout, payload);
856
- if (mutated != null)
857
- payload = { ...mutated, reference: mutated.reference ?? reference };
882
+ if (mutated != null) {
883
+ payload = applyBeforeHookMutation(mutated, payload, preparePayoutPayload);
884
+ }
858
885
  const result = await transport.send({
859
886
  action: SDK_ACTIONS.makePayoutAndResolve,
860
887
  payload
@@ -862,7 +889,7 @@ function createSdkInstance(config) {
862
889
  await runHook(
863
890
  config.hooks?.afterPayout,
864
891
  result.isOk ? slangTs.Ok({ reference: result.value.reference, status: result.value.status }) : slangTs.Err(result.error),
865
- payload
892
+ { ...payload, raw: input }
866
893
  );
867
894
  if (result.isOk) {
868
895
  return slangTs.Ok(result.value);
@@ -896,6 +923,7 @@ function createSdkInstance(config) {
896
923
  async function verifyPhone(input) {
897
924
  validateNonEmpty(input.phoneNumber, "phoneNumber");
898
925
  const normalizedPhone = normalizePhone(input.phoneNumber);
926
+ validatePhoneFormat(normalizedPhone, "phoneNumber");
899
927
  const result = await transport.send({
900
928
  action: SDK_ACTIONS.verifyPhone,
901
929
  payload: { ...input, phoneNumber: normalizedPhone }