@nile-squad/nylonpay-ts 1.0.0 → 1.0.2

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.js CHANGED
@@ -13,7 +13,7 @@ function createEmitter() {
13
13
  if (!state.listeners.has(event)) {
14
14
  state.listeners.set(event, /* @__PURE__ */ new Set());
15
15
  }
16
- state.listeners.get(event).add(handler);
16
+ state.listeners.get(event)?.add(handler);
17
17
  return () => off(event, handler);
18
18
  }
19
19
  function once(event, handler) {
@@ -53,217 +53,24 @@ function createEmitter() {
53
53
  const emitter = { on, once, off, emit, clear, listenerCount };
54
54
  return emitter;
55
55
  }
56
-
57
- // src/payment.ts
58
- var STATUS_TO_EVENT = {
59
- successful: "success",
60
- failed: "failed",
61
- processing: "processing",
62
- cancelled: "cancelled"
63
- };
64
- function statusToEvent(status) {
65
- return STATUS_TO_EVENT[status] ?? null;
56
+ function generateFingerprint() {
57
+ const components = [
58
+ `type:${type()}`,
59
+ `platform:${platform()}`,
60
+ `arch:${arch()}`,
61
+ `release:${release()}`,
62
+ `hostname:${hostname()}`,
63
+ `node:${process.versions.node}`,
64
+ `v8:${process.versions.v8}`
65
+ ].join("|");
66
+ return createHash("sha256").update(components).digest("hex");
66
67
  }
67
- var TERMINAL_STATES = /* @__PURE__ */ new Set([
68
- "successful",
69
- "failed",
70
- "cancelled"
71
- ]);
72
- function createPaymentInstance(initialResponse, deps) {
73
- const state = {
74
- reference: initialResponse.reference,
75
- status: initialResponse.status,
76
- transaction: null,
77
- pollingTimer: null,
78
- resolved: false,
79
- pollAttempts: 0,
80
- pollStartTime: Date.now(),
81
- emitter: createEmitter(),
82
- fetchStatus: deps.fetchStatus,
83
- fetchTransaction: deps.fetchTransaction,
84
- pollIntervalMs: deps.pollIntervalMs ?? 2e3,
85
- maxPollDuration: deps.maxPollDuration ?? 3e5,
86
- maxPollAttempts: deps.maxPollAttempts ?? 150
87
- };
88
- function resolveWithError(error) {
89
- state.resolved = true;
90
- stopPolling();
91
- emitEvent("error", error);
92
- }
93
- function emitEvent(event, error) {
94
- const data = {
95
- event,
96
- transaction: state.transaction ?? void 0,
97
- error,
98
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
99
- };
100
- state.emitter.emit(event, data);
101
- }
102
- async function handleTerminalState(status) {
103
- const txResult = await state.fetchTransaction({
104
- reference: state.reference
105
- });
106
- if (txResult.isOk) {
107
- state.transaction = txResult.value;
108
- const event = statusToEvent(status);
109
- if (event) {
110
- emitEvent(event);
111
- }
112
- } else {
113
- emitEvent("error", `Failed to fetch transaction: ${txResult.error}`);
114
- }
115
- state.resolved = true;
116
- stopPolling();
117
- }
118
- async function handleStatusUpdate(response) {
119
- if (response.reference !== state.reference) {
120
- resolveWithError(
121
- `Reference mismatch: expected ${state.reference} but got ${response.reference}`
122
- );
123
- return;
124
- }
125
- const newStatus = response.status;
126
- const oldStatus = state.status;
127
- state.status = newStatus;
128
- if (newStatus !== oldStatus) {
129
- const event = statusToEvent(newStatus);
130
- if (event) {
131
- if (TERMINAL_STATES.has(newStatus)) {
132
- await handleTerminalState(newStatus);
133
- return;
134
- }
135
- emitEvent(event);
136
- }
137
- }
138
- }
139
- function handlePollError(error) {
140
- const isNotFound = error.includes("not found") || error.includes("NOT_FOUND");
141
- if (isNotFound) {
142
- return;
143
- }
144
- emitEvent("error", error);
145
- state.resolved = true;
146
- stopPolling();
147
- }
148
- function scheduleNextPoll() {
149
- if (state.resolved || state.pollingTimer) {
150
- return;
151
- }
152
- state.pollingTimer = setTimeout(() => {
153
- state.pollingTimer = null;
154
- void pollStatus();
155
- }, state.pollIntervalMs);
156
- }
157
- async function pollStatus() {
158
- if (state.resolved) {
159
- stopPolling();
160
- return;
161
- }
162
- if (state.pollAttempts >= state.maxPollAttempts) {
163
- resolveWithError("Polling timeout: exceeded maximum attempts");
164
- return;
165
- }
166
- if (Date.now() - state.pollStartTime >= state.maxPollDuration) {
167
- resolveWithError("Polling timeout: exceeded maximum duration");
168
- return;
169
- }
170
- state.pollAttempts += 1;
171
- const result = await state.fetchStatus({ reference: state.reference });
172
- if (result.isOk) {
173
- await handleStatusUpdate(result.value);
174
- } else {
175
- handlePollError(result.error);
176
- }
177
- if (state.resolved) {
178
- stopPolling();
179
- return;
180
- }
181
- scheduleNextPoll();
182
- }
183
- function startPolling() {
184
- scheduleNextPoll();
185
- }
186
- function stopPolling() {
187
- if (state.pollingTimer) {
188
- clearTimeout(state.pollingTimer);
189
- state.pollingTimer = null;
190
- }
191
- }
192
- function on(event, handler) {
193
- state.emitter.on(event, handler);
194
- return paymentInstance;
195
- }
196
- function off(event, handler) {
197
- state.emitter.off(event, handler);
198
- return paymentInstance;
199
- }
200
- function once(event, handler) {
201
- state.emitter.once(event, handler);
202
- return paymentInstance;
203
- }
204
- function wait() {
205
- return new Promise((resolve, reject) => {
206
- if (state.resolved) {
207
- if (state.status === "successful" && state.transaction) {
208
- resolve(state.transaction);
209
- } else {
210
- reject(new Error(`Payment ${state.status ?? "error"}`));
211
- }
212
- return;
213
- }
214
- function onSuccess() {
215
- cleanup();
216
- if (state.transaction) {
217
- resolve(state.transaction);
218
- } else {
219
- reject(
220
- new Error("Payment successful but transaction data unavailable")
221
- );
222
- }
223
- }
224
- function onFailed() {
225
- cleanup();
226
- reject(new Error("Payment failed"));
227
- }
228
- function onCancelled() {
229
- cleanup();
230
- reject(new Error("Payment cancelled"));
231
- }
232
- function onError(data) {
233
- cleanup();
234
- const eventData = data;
235
- reject(new Error(eventData.error ?? "Payment error"));
236
- }
237
- function cleanup() {
238
- state.emitter.off("success", onSuccess);
239
- state.emitter.off("failed", onFailed);
240
- state.emitter.off("cancelled", onCancelled);
241
- state.emitter.off("error", onError);
242
- }
243
- state.emitter.on("success", onSuccess);
244
- state.emitter.on("failed", onFailed);
245
- state.emitter.on("cancelled", onCancelled);
246
- state.emitter.on("error", onError);
247
- });
248
- }
249
- const paymentInstance = {
250
- get reference() {
251
- return state.reference;
252
- },
253
- get status() {
254
- return state.status;
255
- },
256
- on,
257
- once,
258
- off,
259
- wait
260
- };
261
- startPolling();
262
- return paymentInstance;
68
+ function generateNonce(length = 16) {
69
+ return randomBytes(length).toString("hex");
263
70
  }
264
71
 
265
72
  // src/sdk.config.ts
266
- var DEFAULT_BASE_URL = "https://api.nylonpay.io/api/services";
73
+ var DEFAULT_BASE_URL = "https://api.nylonpay.nilesquad.com/api/services";
267
74
  var DEFAULT_TIMEOUT_MS = 3e4;
268
75
  var DEFAULT_MAX_RETRIES = 3;
269
76
  var DEFAULT_MAX_POLL_INTERVAL_MS = 2e3;
@@ -281,21 +88,6 @@ var SDK_ACTIONS = {
281
88
  createInvoice: "sdk-create-invoice"
282
89
  };
283
90
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
284
- function generateFingerprint() {
285
- const components = [
286
- `type:${type()}`,
287
- `platform:${platform()}`,
288
- `arch:${arch()}`,
289
- `release:${release()}`,
290
- `hostname:${hostname()}`,
291
- `node:${process.versions.node}`,
292
- `v8:${process.versions.v8}`
293
- ].join("|");
294
- return createHash("sha256").update(components).digest("hex");
295
- }
296
- function generateNonce(length = 16) {
297
- return randomBytes(length).toString("hex");
298
- }
299
91
  function sortValue(value) {
300
92
  if (Array.isArray(value)) {
301
93
  return value.map((entry) => sortValue(entry));
@@ -515,6 +307,214 @@ function parseError(error) {
515
307
  }
516
308
  return { code: "UNKNOWN", message: error };
517
309
  }
310
+
311
+ // src/payment.ts
312
+ var STATUS_TO_EVENT = {
313
+ successful: "success",
314
+ failed: "failed",
315
+ processing: "processing",
316
+ cancelled: "cancelled"
317
+ };
318
+ function statusToEvent(status) {
319
+ return STATUS_TO_EVENT[status] ?? null;
320
+ }
321
+ var TERMINAL_STATES = /* @__PURE__ */ new Set([
322
+ "successful",
323
+ "failed",
324
+ "cancelled"
325
+ ]);
326
+ function createPaymentInstance(initialResponse, deps) {
327
+ const state = {
328
+ reference: initialResponse.reference,
329
+ status: initialResponse.status,
330
+ transaction: null,
331
+ pollingTimer: null,
332
+ resolved: false,
333
+ pollAttempts: 0,
334
+ pollStartTime: Date.now(),
335
+ emitter: createEmitter(),
336
+ fetchStatus: deps.fetchStatus,
337
+ fetchTransaction: deps.fetchTransaction,
338
+ pollIntervalMs: deps.pollIntervalMs ?? 2e3,
339
+ maxPollDuration: deps.maxPollDuration ?? 3e5,
340
+ maxPollAttempts: deps.maxPollAttempts ?? 150
341
+ };
342
+ function resolveWithError(error) {
343
+ state.resolved = true;
344
+ stopPolling();
345
+ emitEvent("error", parseError(error).message);
346
+ }
347
+ function emitEvent(event, error) {
348
+ const data = {
349
+ event,
350
+ transaction: state.transaction ?? void 0,
351
+ error,
352
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
353
+ };
354
+ state.emitter.emit(event, data);
355
+ }
356
+ async function handleTerminalState(status) {
357
+ const txResult = await state.fetchTransaction({
358
+ reference: state.reference
359
+ });
360
+ if (txResult.isOk) {
361
+ state.transaction = txResult.value;
362
+ const event = statusToEvent(status);
363
+ if (event) {
364
+ emitEvent(event);
365
+ }
366
+ } else {
367
+ emitEvent("error", `Failed to fetch transaction: ${txResult.error}`);
368
+ }
369
+ state.resolved = true;
370
+ stopPolling();
371
+ }
372
+ async function handleStatusUpdate(response) {
373
+ if (response.reference !== state.reference) {
374
+ resolveWithError(
375
+ `Reference mismatch: expected ${state.reference} but got ${response.reference}`
376
+ );
377
+ return;
378
+ }
379
+ const newStatus = response.status;
380
+ const oldStatus = state.status;
381
+ state.status = newStatus;
382
+ if (newStatus !== oldStatus) {
383
+ const event = statusToEvent(newStatus);
384
+ if (event) {
385
+ if (TERMINAL_STATES.has(newStatus)) {
386
+ await handleTerminalState(newStatus);
387
+ return;
388
+ }
389
+ emitEvent(event);
390
+ }
391
+ }
392
+ }
393
+ function handlePollError(error) {
394
+ const isNotFound = error.includes("not found") || error.includes("NOT_FOUND");
395
+ if (isNotFound) {
396
+ return;
397
+ }
398
+ emitEvent("error", parseError(error).message);
399
+ state.resolved = true;
400
+ stopPolling();
401
+ }
402
+ function scheduleNextPoll() {
403
+ if (state.resolved || state.pollingTimer) {
404
+ return;
405
+ }
406
+ state.pollingTimer = setTimeout(() => {
407
+ state.pollingTimer = null;
408
+ void pollStatus();
409
+ }, state.pollIntervalMs);
410
+ }
411
+ async function pollStatus() {
412
+ if (state.resolved) {
413
+ stopPolling();
414
+ return;
415
+ }
416
+ if (state.pollAttempts >= state.maxPollAttempts) {
417
+ resolveWithError("Polling timeout: exceeded maximum attempts");
418
+ return;
419
+ }
420
+ if (Date.now() - state.pollStartTime >= state.maxPollDuration) {
421
+ resolveWithError("Polling timeout: exceeded maximum duration");
422
+ return;
423
+ }
424
+ state.pollAttempts += 1;
425
+ const result = await state.fetchStatus({ reference: state.reference });
426
+ if (result.isOk) {
427
+ await handleStatusUpdate(result.value);
428
+ } else {
429
+ handlePollError(result.error);
430
+ }
431
+ if (state.resolved) {
432
+ stopPolling();
433
+ return;
434
+ }
435
+ scheduleNextPoll();
436
+ }
437
+ function startPolling() {
438
+ scheduleNextPoll();
439
+ }
440
+ function stopPolling() {
441
+ if (state.pollingTimer) {
442
+ clearTimeout(state.pollingTimer);
443
+ state.pollingTimer = null;
444
+ }
445
+ }
446
+ function on(event, handler) {
447
+ state.emitter.on(event, handler);
448
+ return paymentInstance;
449
+ }
450
+ function off(event, handler) {
451
+ state.emitter.off(event, handler);
452
+ return paymentInstance;
453
+ }
454
+ function once(event, handler) {
455
+ state.emitter.once(event, handler);
456
+ return paymentInstance;
457
+ }
458
+ function wait() {
459
+ return new Promise((resolve, reject) => {
460
+ if (state.resolved) {
461
+ if (state.status === "successful" && state.transaction) {
462
+ resolve(state.transaction);
463
+ } else {
464
+ reject(new Error(`Payment ${state.status ?? "error"}`));
465
+ }
466
+ return;
467
+ }
468
+ function onSuccess() {
469
+ cleanup();
470
+ if (state.transaction) {
471
+ resolve(state.transaction);
472
+ } else {
473
+ reject(
474
+ new Error("Payment successful but transaction data unavailable")
475
+ );
476
+ }
477
+ }
478
+ function onFailed() {
479
+ cleanup();
480
+ reject(new Error("Payment failed"));
481
+ }
482
+ function onCancelled() {
483
+ cleanup();
484
+ reject(new Error("Payment cancelled"));
485
+ }
486
+ function onError(data) {
487
+ cleanup();
488
+ const eventData = data;
489
+ reject(new Error(eventData.error ?? "Payment error"));
490
+ }
491
+ function cleanup() {
492
+ state.emitter.off("success", onSuccess);
493
+ state.emitter.off("failed", onFailed);
494
+ state.emitter.off("cancelled", onCancelled);
495
+ state.emitter.off("error", onError);
496
+ }
497
+ state.emitter.on("success", onSuccess);
498
+ state.emitter.on("failed", onFailed);
499
+ state.emitter.on("cancelled", onCancelled);
500
+ state.emitter.on("error", onError);
501
+ });
502
+ }
503
+ const paymentInstance = {
504
+ get reference() {
505
+ return state.reference;
506
+ },
507
+ get status() {
508
+ return state.status;
509
+ },
510
+ on,
511
+ once,
512
+ off,
513
+ wait
514
+ };
515
+ startPolling();
516
+ return paymentInstance;
517
+ }
518
518
  function verifyWebhookSignature(input) {
519
519
  const payloadBytes = typeof input.payload === "string" ? Buffer.from(input.payload, "utf8") : Buffer.from(input.payload);
520
520
  const expectedSignature = createHmac("sha256", input.secret).update(payloadBytes).digest("hex");
@@ -571,11 +571,25 @@ function createSdkInstance(config) {
571
571
  if (input.method === "bank" && !input.bank) {
572
572
  throw new Error('bank details are required when method is "bank"');
573
573
  }
574
- const payload = { ...input, reference };
574
+ let payload = { ...input, reference };
575
+ if (config.hooks?.beforeCollect) {
576
+ const mutated = await config.hooks.beforeCollect(payload);
577
+ if (mutated != null)
578
+ payload = { ...mutated, reference: mutated.reference ?? reference };
579
+ }
575
580
  const result = await transport.send({
576
581
  action: SDK_ACTIONS.collectPayment,
577
582
  payload
578
583
  });
584
+ if (config.hooks?.afterCollect) {
585
+ await config.hooks.afterCollect(
586
+ result.isOk ? Ok({
587
+ reference: result.value.reference,
588
+ status: result.value.status
589
+ }) : Err(result.error),
590
+ payload
591
+ );
592
+ }
579
593
  if (result.isOk) {
580
594
  return createPaymentInstance(result.value, commonDeps);
581
595
  }
@@ -599,11 +613,25 @@ function createSdkInstance(config) {
599
613
  if (input.method === "bank" && !input.bank) {
600
614
  throw new Error('bank details are required when method is "bank"');
601
615
  }
602
- const payload = { ...input, reference };
616
+ let payload = { ...input, reference };
617
+ if (config.hooks?.beforeCollect) {
618
+ const mutated = await config.hooks.beforeCollect(payload);
619
+ if (mutated != null)
620
+ payload = { ...mutated, reference: mutated.reference ?? reference };
621
+ }
603
622
  const result = await transport.send({
604
623
  action: SDK_ACTIONS.collectPaymentAndResolve,
605
624
  payload
606
625
  });
626
+ if (config.hooks?.afterCollect) {
627
+ await config.hooks.afterCollect(
628
+ result.isOk ? Ok({
629
+ reference: result.value.reference,
630
+ status: result.value.status
631
+ }) : Err(result.error),
632
+ payload
633
+ );
634
+ }
607
635
  if (result.isOk) {
608
636
  return Ok(result.value);
609
637
  }
@@ -623,11 +651,25 @@ function createSdkInstance(config) {
623
651
  input.destination.accountNumber,
624
652
  "destination.accountNumber"
625
653
  );
626
- const payload = { ...input, reference };
654
+ let payload = { ...input, reference };
655
+ if (config.hooks?.beforePayout) {
656
+ const mutated = await config.hooks.beforePayout(payload);
657
+ if (mutated != null)
658
+ payload = { ...mutated, reference: mutated.reference ?? reference };
659
+ }
627
660
  const result = await transport.send({
628
661
  action: SDK_ACTIONS.makePayout,
629
662
  payload
630
663
  });
664
+ if (config.hooks?.afterPayout) {
665
+ await config.hooks.afterPayout(
666
+ result.isOk ? Ok({
667
+ reference: result.value.reference,
668
+ status: result.value.status
669
+ }) : Err(result.error),
670
+ payload
671
+ );
672
+ }
631
673
  if (result.isOk) {
632
674
  return createPaymentInstance(result.value, commonDeps);
633
675
  }
@@ -656,11 +698,25 @@ function createSdkInstance(config) {
656
698
  input.destination.accountNumber,
657
699
  "destination.accountNumber"
658
700
  );
659
- const payload = { ...input, reference };
701
+ let payload = { ...input, reference };
702
+ if (config.hooks?.beforePayout) {
703
+ const mutated = await config.hooks.beforePayout(payload);
704
+ if (mutated != null)
705
+ payload = { ...mutated, reference: mutated.reference ?? reference };
706
+ }
660
707
  const result = await transport.send({
661
708
  action: SDK_ACTIONS.makePayoutAndResolve,
662
709
  payload
663
710
  });
711
+ if (config.hooks?.afterPayout) {
712
+ await config.hooks.afterPayout(
713
+ result.isOk ? Ok({
714
+ reference: result.value.reference,
715
+ status: result.value.status
716
+ }) : Err(result.error),
717
+ payload
718
+ );
719
+ }
664
720
  if (result.isOk) {
665
721
  return Ok(result.value);
666
722
  }
@@ -745,6 +801,7 @@ function createSdkInstance(config) {
745
801
  }
746
802
 
747
803
  // src/create-nylon-pay.ts
804
+ var instances = /* @__PURE__ */ new Map();
748
805
  function createNylonPay(config) {
749
806
  if (!config.apiKey) {
750
807
  throw new Error("apiKey is required");
@@ -758,18 +815,27 @@ function createNylonPay(config) {
758
815
  if (!config.apiSecret.startsWith("nps_")) {
759
816
  throw new Error('apiSecret must start with "nps_"');
760
817
  }
818
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
819
+ const instanceKey = `${config.apiKey}:${baseUrl}`;
820
+ if (!config.force) {
821
+ const existing = instances.get(instanceKey);
822
+ if (existing) return existing;
823
+ }
761
824
  const resolvedConfig = {
762
825
  apiKey: config.apiKey,
763
826
  apiSecret: config.apiSecret,
764
- baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
827
+ baseUrl,
765
828
  timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
766
829
  maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
767
830
  maxPollIntervalMs: config.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS,
768
831
  maxPollDurationMs: config.maxPollDurationMs ?? DEFAULT_MAX_POLL_DURATION_MS,
769
832
  maxPollAttempts: config.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS,
770
- fetch: config.fetch ?? globalThis.fetch.bind(globalThis)
833
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
834
+ hooks: config.hooks
771
835
  };
772
- return createSdkInstance(resolvedConfig);
836
+ const instance = createSdkInstance(resolvedConfig);
837
+ instances.set(instanceKey, instance);
838
+ return instance;
773
839
  }
774
840
 
775
841
  export { createNylonPay, parseError, verifyWebhookSignature };