@munchi_oy/payments 1.6.10 → 1.10.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.
@@ -59,5 +59,14 @@ export interface SDKOptions {
59
59
  */
60
60
  autoResetOnPaymentComplete?: AutoResetOptions;
61
61
  appToApp?: AppToAppConfig;
62
+ /**
63
+ * Routes POS Viva payments through the backend payment-session flow
64
+ * (VivaSessionStrategy) instead of the legacy v4 VivaStrategy. Kiosk/handheld
65
+ * leave this unset and stay on the legacy flow.
66
+ */
67
+ paymentSession?: PaymentSessionConfig;
68
+ }
69
+ export interface PaymentSessionConfig {
70
+ enabled: boolean;
62
71
  }
63
72
  //# sourceMappingURL=sdk.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../../src/types/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9E,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC7D;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnD,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACtD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnD,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAClD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC5C,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACnD;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AACvD,MAAM,MAAM,2BAA2B,GAAG,UAAU,GAAG,aAAa,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACvD,aAAa,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,2BAA2B,CAAC;IAClD,OAAO,EAAE,eAAe,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,0BAA0B,CAAC,EAAE,gBAAgB,CAAC;IAC9C,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B"}
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../../src/types/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9E,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC7D;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnD,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACtD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnD,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAClD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC5C,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACnD;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AACvD,MAAM,MAAM,2BAA2B,GAAG,UAAU,GAAG,aAAa,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACvD,aAAa,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,2BAA2B,CAAC;IAClD,OAAO,EAAE,eAAe,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,0BAA0B,CAAC,EAAE,gBAAgB,CAAC;IAC9C,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;CAClB"}
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "1.6.10";
1
+ export declare const VERSION = "1.10.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@munchi_oy/payments",
3
- "version": "1.6.10",
3
+ "version": "1.10.0",
4
4
  "description": "Munchi Payments SDK - Payment processing utilities",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -32,7 +32,7 @@
32
32
  "@types/jest": "^30.0.0",
33
33
  "tsup": "^8.0.0",
34
34
  "typescript": "^5.0.0",
35
- "@munchi_oy/core": "1.9.11"
35
+ "@munchi_oy/core": "1.9.38"
36
36
  },
37
37
  "scripts": {
38
38
  "test": "jest",
@@ -4,13 +4,16 @@ import { version } from "../package.json";
4
4
  import { PaymentErrorCode, PaymentSDKError } from "./error";
5
5
  import type { IPaymentStrategy } from "./strategies/IPaymentStrategy";
6
6
  import { StrategyExecutionMode } from "./strategies/IPaymentStrategy";
7
+ import { ManualSessionStrategy } from "./strategies/ManualSessionStrategy";
7
8
  import { NetsStrategy } from "./strategies/NetsStrategy";
8
9
  import { VivaAppToAppStrategy } from "./strategies/VivaAppToAppStrategy";
10
+ import { VivaSessionStrategy } from "./strategies/VivaSessionStrategy";
9
11
  import { VivaStrategy } from "./strategies/VivaStrategy";
10
12
  import { WorldlineStrategy } from "./strategies/WorldlineStrategy";
11
13
  import {
12
14
  type IMessagingAdapter,
13
15
  type IMunchiPaymentSDK,
16
+ type ManualPaymentRequest,
14
17
  PaymentInteractionState,
15
18
  type PaymentResult,
16
19
  type PaymentTerminalConfig,
@@ -18,8 +21,14 @@ import {
18
21
  type PaymentRequest as SdkPaymentRequest,
19
22
  SdkPaymentStatus,
20
23
  type TransactionOptions,
24
+ type VoidRequest,
21
25
  } from "./types/payment";
22
- import type { AppToAppConfig, ILogger, SDKOptions } from "./types/sdk";
26
+ import type {
27
+ AppToAppConfig,
28
+ ILogger,
29
+ PaymentSessionConfig,
30
+ SDKOptions,
31
+ } from "./types/sdk";
23
32
 
24
33
  type StateListener = (state: PaymentInteractionState) => void;
25
34
 
@@ -36,6 +45,8 @@ export class MunchiPaymentSDK implements IMunchiPaymentSDK {
36
45
  private _autoResetTimer: ReturnType<typeof setTimeout> | undefined;
37
46
  private autoResetOptions: SDKOptions["autoResetOnPaymentComplete"];
38
47
  private appToAppConfig: AppToAppConfig | undefined;
48
+ private paymentSessionConfig: PaymentSessionConfig | undefined;
49
+ private manualStrategy: ManualSessionStrategy;
39
50
 
40
51
  private static readonly TERMINAL_STATES = [
41
52
  PaymentInteractionState.SUCCESS,
@@ -61,6 +72,8 @@ export class MunchiPaymentSDK implements IMunchiPaymentSDK {
61
72
  this.timeoutMs = options.timeoutMs || 30000;
62
73
  this.autoResetOptions = options.autoResetOnPaymentComplete;
63
74
  this.appToAppConfig = options.appToApp;
75
+ this.paymentSessionConfig = options.paymentSession;
76
+ this.manualStrategy = new ManualSessionStrategy(axios, config);
64
77
  this.strategy = strategy ?? this.resolveStrategy(config);
65
78
  }
66
79
 
@@ -200,6 +213,9 @@ export class MunchiPaymentSDK implements IMunchiPaymentSDK {
200
213
  this.appToAppConfig,
201
214
  );
202
215
  }
216
+ if (this.paymentSessionConfig?.enabled) {
217
+ return new VivaSessionStrategy(this.axios, this.messaging, config);
218
+ }
203
219
  return new VivaStrategy(this.axios, this.messaging, config);
204
220
 
205
221
  default:
@@ -645,4 +661,118 @@ export class MunchiPaymentSDK implements IMunchiPaymentSDK {
645
661
  return errorResult;
646
662
  }
647
663
  };
664
+
665
+ public recordManualPayment = async (
666
+ params: ManualPaymentRequest,
667
+ options?: TransactionOptions,
668
+ ): Promise<PaymentResult> => {
669
+ const callbacks = options ?? {};
670
+ this.logger?.info("Recording manual payment", {
671
+ orderRef: params.orderRef,
672
+ });
673
+ this._currentSessionId = undefined;
674
+ this._cancellationIntent = false;
675
+
676
+ if (!MunchiPaymentSDK.RESTING_STATES.includes(this._currentState)) {
677
+ return this.generateErrorResult(
678
+ params.orderRef,
679
+ PaymentErrorCode.UNKNOWN,
680
+ "A transaction is already in progress",
681
+ );
682
+ }
683
+
684
+ this.transitionTo(PaymentInteractionState.CONNECTING);
685
+ this.safeFireCallback(() =>
686
+ callbacks.onConnecting?.({
687
+ orderRef: params.orderRef,
688
+ refPaymentId: undefined,
689
+ }),
690
+ );
691
+
692
+ try {
693
+ const result = await this.manualStrategy.recordPayment(params);
694
+ this.settleSingleShot(result, params.orderRef, callbacks);
695
+ return result;
696
+ } catch (error) {
697
+ this.logger?.error("Manual payment failed", error);
698
+ return this.failSingleShot(
699
+ params.orderRef,
700
+ error,
701
+ "Manual payment failed",
702
+ callbacks,
703
+ );
704
+ }
705
+ };
706
+
707
+ public voidManualPayment = async (
708
+ params: VoidRequest,
709
+ options?: TransactionOptions,
710
+ ): Promise<PaymentResult> => {
711
+ const callbacks = options ?? {};
712
+ this.logger?.info("Voiding manual payment", { orderRef: params.orderRef });
713
+ this._currentSessionId = params.sessionId;
714
+ this._cancellationIntent = false;
715
+
716
+ if (!MunchiPaymentSDK.RESTING_STATES.includes(this._currentState)) {
717
+ return this.generateErrorResult(
718
+ params.orderRef,
719
+ PaymentErrorCode.UNKNOWN,
720
+ "A transaction is already in progress",
721
+ );
722
+ }
723
+
724
+ this.transitionTo(PaymentInteractionState.CONNECTING);
725
+
726
+ try {
727
+ const result = await this.manualStrategy.voidPayment(params);
728
+ this.settleSingleShot(result, params.orderRef, callbacks);
729
+ return result;
730
+ } catch (error) {
731
+ this.logger?.error("Manual void failed", error);
732
+ return this.failSingleShot(
733
+ params.orderRef,
734
+ error,
735
+ "Void failed",
736
+ callbacks,
737
+ );
738
+ }
739
+ };
740
+
741
+ private settleSingleShot(
742
+ result: PaymentResult,
743
+ orderRef: string,
744
+ callbacks: TransactionOptions,
745
+ ): void {
746
+ if (result.success) {
747
+ this.transitionTo(PaymentInteractionState.SUCCESS);
748
+ this.safeFireCallback(() => callbacks.onSuccess?.(result));
749
+ } else if (result.status === SdkPaymentStatus.CANCELLED) {
750
+ this.transitionTo(PaymentInteractionState.FAILED);
751
+ this.safeFireCallback(() =>
752
+ callbacks.onCancelled?.({
753
+ orderRef,
754
+ refPaymentId: this._currentSessionId,
755
+ }),
756
+ );
757
+ } else {
758
+ this.transitionTo(PaymentInteractionState.FAILED);
759
+ this.safeFireCallback(() => callbacks.onError?.(result));
760
+ }
761
+ }
762
+
763
+ private failSingleShot(
764
+ orderRef: string,
765
+ error: unknown,
766
+ fallbackMessage: string,
767
+ callbacks: TransactionOptions,
768
+ ): PaymentResult {
769
+ const errorResult = this.generateErrorResult(
770
+ orderRef,
771
+ PaymentErrorCode.NETWORK_ERROR,
772
+ error instanceof Error ? error.message : fallbackMessage,
773
+ );
774
+ this.transitionTo(PaymentInteractionState.FAILED);
775
+ this.safeFireCallback(() => callbacks.onError?.(errorResult));
776
+ return errorResult;
777
+ }
648
778
  }
@@ -0,0 +1,93 @@
1
+ import {
2
+ type CreateManualSessionDto,
3
+ PaymentSessionApi,
4
+ PaymentSessionStatus,
5
+ } from "@munchi_oy/core";
6
+ import type { AxiosInstance } from "axios";
7
+
8
+ import { PaymentErrorCode, PaymentSDKError } from "../error";
9
+ import {
10
+ type ManualPaymentRequest,
11
+ type PaymentResult,
12
+ type PaymentTerminalConfig,
13
+ SdkPaymentStatus,
14
+ type VoidRequest,
15
+ } from "../types/payment";
16
+ import { mapSessionResult } from "./sessionResult";
17
+
18
+ export class ManualSessionStrategy {
19
+ private sessionApi: PaymentSessionApi;
20
+
21
+ constructor(
22
+ axios: AxiosInstance,
23
+ private config: PaymentTerminalConfig,
24
+ ) {
25
+ this.sessionApi = new PaymentSessionApi(undefined, "", axios);
26
+ }
27
+
28
+ async recordPayment(request: ManualPaymentRequest): Promise<PaymentResult> {
29
+ try {
30
+ const payload: CreateManualSessionDto = {
31
+ businessId: Number(this.config.storeId),
32
+ orderId: request.orderRef,
33
+ amount: request.amountCents,
34
+ paymethod: request.paymethod,
35
+ provider: this.config.provider,
36
+ };
37
+ if (request.tipAmount !== undefined) {
38
+ payload.tipAmount = request.tipAmount;
39
+ }
40
+ if (request.splitContext !== undefined) {
41
+ payload.splitContext = request.splitContext;
42
+ }
43
+ if (request.metadata !== undefined) {
44
+ payload.metadata = request.metadata;
45
+ }
46
+
47
+ const { data } = await this.sessionApi.recordManualPayment(payload);
48
+ return mapSessionResult(data, request.orderRef);
49
+ } catch (error) {
50
+ throw new PaymentSDKError(
51
+ PaymentErrorCode.NETWORK_ERROR,
52
+ "Failed to record manual payment",
53
+ error,
54
+ );
55
+ }
56
+ }
57
+
58
+ async voidPayment(request: VoidRequest): Promise<PaymentResult> {
59
+ try {
60
+ const { data } = await this.sessionApi.voidManualPayment({
61
+ sessionId: request.sessionId,
62
+ });
63
+
64
+ const isVoided =
65
+ data.status === PaymentSessionStatus.Voided ||
66
+ data.status === PaymentSessionStatus.Voiding;
67
+
68
+ const result: PaymentResult = {
69
+ success: isVoided,
70
+ status: isVoided ? SdkPaymentStatus.SUCCESS : SdkPaymentStatus.FAILED,
71
+ orderId: request.orderRef,
72
+ transactionId: data.providerTransactionId ?? data.sessionId,
73
+ };
74
+
75
+ if (!isVoided) {
76
+ if (data.errorCode) {
77
+ result.errorCode = data.errorCode;
78
+ }
79
+ if (data.errorMessage) {
80
+ result.errorMessage = data.errorMessage;
81
+ }
82
+ }
83
+
84
+ return result;
85
+ } catch (error) {
86
+ throw new PaymentSDKError(
87
+ PaymentErrorCode.NETWORK_ERROR,
88
+ "Failed to void manual payment",
89
+ error,
90
+ );
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,367 @@
1
+ import {
2
+ type InitiateVivaSessionDto,
3
+ PaymentFailureCode,
4
+ PaymentSessionApi,
5
+ PaymentSessionStatus,
6
+ type PaymentSessionViewDto,
7
+ type PaymentStatusDto,
8
+ type ReverseSessionDto,
9
+ SimplePaymentStatus,
10
+ type TransactionDto,
11
+ } from "@munchi_oy/core";
12
+ import type { AxiosInstance } from "axios";
13
+
14
+ import { PaymentErrorCode, PaymentSDKError } from "../error";
15
+ import {
16
+ type IMessagingAdapter,
17
+ type PaymentInteractionState,
18
+ PaymentInteractionState as InteractionState,
19
+ type PaymentRequest,
20
+ type PaymentResult,
21
+ type PaymentTerminalConfig,
22
+ type RefundRequest,
23
+ SdkPaymentStatus,
24
+ } from "../types/payment";
25
+ import type { IPaymentStrategy } from "./IPaymentStrategy";
26
+ import { mapSessionResult, pendingResult } from "./sessionResult";
27
+
28
+ export class VivaSessionStrategy implements IPaymentStrategy {
29
+ private static readonly POLLING_DURATION_MS = 180000;
30
+ private static readonly POLLING_INTERVAL_MS = 1000;
31
+ private static readonly VERIFY_DURATION_MS = 30000;
32
+ private static readonly INITIAL_POLL_DELAY_MS = 10000;
33
+
34
+ private sessionApi: PaymentSessionApi;
35
+ private abortController: AbortController | null = null;
36
+ private idempotencyKey: string | null = null;
37
+ private idempotencyOrderRef: string | null = null;
38
+ private currentClientToken: string | null = null;
39
+
40
+ constructor(
41
+ axios: AxiosInstance,
42
+ private messaging: IMessagingAdapter,
43
+ private config: PaymentTerminalConfig,
44
+ ) {
45
+ this.sessionApi = new PaymentSessionApi(undefined, "", axios);
46
+ }
47
+
48
+ async processPayment(
49
+ request: PaymentRequest,
50
+ onStateChange: (state: PaymentInteractionState, detail?: { sessionId?: string }) => void,
51
+ ): Promise<PaymentResult> {
52
+ this.abortController = new AbortController();
53
+ if (
54
+ this.idempotencyKey === null ||
55
+ this.idempotencyOrderRef !== request.orderRef
56
+ ) {
57
+ this.idempotencyKey = this.generateIdempotencyKey();
58
+ this.idempotencyOrderRef = request.orderRef;
59
+ }
60
+ this.currentClientToken = null;
61
+
62
+ onStateChange(InteractionState.CONNECTING);
63
+
64
+ const payload: InitiateVivaSessionDto = {
65
+ amount: request.amountCents,
66
+ businessId: Number(this.config.storeId),
67
+ orderId: request.orderRef,
68
+ };
69
+ if (request.splitContext !== undefined) {
70
+ payload.splitContext = request.splitContext;
71
+ }
72
+
73
+ try {
74
+ const { data } = await this.sessionApi.initiateVivaSession(
75
+ payload,
76
+ this.idempotencyKey,
77
+ );
78
+ this.currentClientToken = data.clientToken;
79
+ const signal = this.abortController.signal;
80
+
81
+ if (signal.aborted) {
82
+ throw new Error("Aborted");
83
+ }
84
+
85
+ onStateChange(InteractionState.REQUIRES_INPUT, { sessionId: data.sessionId });
86
+
87
+ const terminal = await this.waitForCompletion(
88
+ data.clientToken,
89
+ data.sessionId,
90
+ request,
91
+ signal,
92
+ );
93
+
94
+ if (terminal) {
95
+ return this.finalize(terminal);
96
+ }
97
+
98
+ await this.armCancel(data.clientToken);
99
+ return pendingResult(request.orderRef);
100
+ } catch (err) {
101
+ if (err instanceof PaymentSDKError) throw err;
102
+ throw new PaymentSDKError(
103
+ PaymentErrorCode.NETWORK_ERROR,
104
+ "Failed to create Viva payment session",
105
+ err,
106
+ );
107
+ }
108
+ }
109
+
110
+ async verifyFinalStatus(
111
+ request: PaymentRequest,
112
+ _sessionId: string,
113
+ ): Promise<PaymentResult> {
114
+ if (!this.currentClientToken) {
115
+ throw new PaymentSDKError(
116
+ PaymentErrorCode.NETWORK_ERROR,
117
+ "No active payment session to verify",
118
+ );
119
+ }
120
+
121
+ const terminal = await this.pollUntilTerminal(
122
+ this.currentClientToken,
123
+ request,
124
+ undefined,
125
+ VivaSessionStrategy.VERIFY_DURATION_MS,
126
+ );
127
+
128
+ return this.finalize(terminal ?? pendingResult(request.orderRef));
129
+ }
130
+
131
+ async cancelTransaction(
132
+ _onStateChange: (state: PaymentInteractionState) => void,
133
+ ): Promise<boolean> {
134
+ if (!this.currentClientToken) {
135
+ return false;
136
+ }
137
+
138
+ try {
139
+ await this.sessionApi.cancelVivaSession(this.currentClientToken);
140
+ } catch (error) {
141
+ throw new PaymentSDKError(
142
+ PaymentErrorCode.NETWORK_ERROR,
143
+ "Failed to cancel Viva payment session",
144
+ error,
145
+ );
146
+ }
147
+
148
+ this.abortController?.abort();
149
+ return true;
150
+ }
151
+
152
+ async refundTransaction(
153
+ request: RefundRequest,
154
+ _onStateChange: (state: PaymentInteractionState, detail?: { sessionId?: string }) => void,
155
+ ): Promise<PaymentResult> {
156
+ try {
157
+ const payload: ReverseSessionDto = {
158
+ sessionId: request.originalTransactionId,
159
+ amount: request.amountCents,
160
+ };
161
+
162
+ const { data } = await this.sessionApi.refundVivaSession(payload);
163
+
164
+ const isRefunded =
165
+ data.status === PaymentSessionStatus.Refunded ||
166
+ data.status === PaymentSessionStatus.Refunding;
167
+
168
+ const result: PaymentResult = {
169
+ success: isRefunded,
170
+ status: isRefunded ? SdkPaymentStatus.SUCCESS : SdkPaymentStatus.FAILED,
171
+ orderId: request.orderRef,
172
+ transactionId: data.providerTransactionId ?? data.sessionId,
173
+ };
174
+
175
+ if (!isRefunded) {
176
+ if (data.errorCode) result.errorCode = data.errorCode;
177
+ if (data.errorMessage) result.errorMessage = data.errorMessage;
178
+ }
179
+
180
+ return result;
181
+ } catch (error) {
182
+ throw new PaymentSDKError(
183
+ PaymentErrorCode.NETWORK_ERROR,
184
+ "Failed to refund Viva transaction",
185
+ error,
186
+ );
187
+ }
188
+ }
189
+
190
+ abort(): void {
191
+ this.abortController?.abort();
192
+ }
193
+
194
+ private async pollUntilTerminal(
195
+ clientToken: string,
196
+ request: PaymentRequest,
197
+ signal: AbortSignal | undefined,
198
+ durationMs: number,
199
+ ): Promise<PaymentResult | undefined> {
200
+ const deadline = Date.now() + durationMs;
201
+
202
+ while (Date.now() < deadline) {
203
+ if (signal?.aborted) {
204
+ throw new Error("Aborted");
205
+ }
206
+
207
+ let data: PaymentSessionViewDto | undefined;
208
+ try {
209
+ data = (await this.sessionApi.getPaymentSession(clientToken)).data;
210
+ } catch (error) {
211
+ if (error instanceof Error && error.message === "Aborted") throw error;
212
+ }
213
+
214
+ if (signal?.aborted) {
215
+ throw new Error("Aborted");
216
+ }
217
+
218
+ if (data) {
219
+ const result = mapSessionResult(data, request.orderRef);
220
+ if (result.status !== SdkPaymentStatus.PENDING) {
221
+ return result;
222
+ }
223
+ }
224
+
225
+ await this.delay(VivaSessionStrategy.POLLING_INTERVAL_MS, signal);
226
+ }
227
+
228
+ return undefined;
229
+ }
230
+
231
+ private waitForCompletion(
232
+ clientToken: string,
233
+ sessionId: string,
234
+ request: PaymentRequest,
235
+ signal: AbortSignal,
236
+ ): Promise<PaymentResult | undefined> {
237
+ const channelName = `viva.${this.config.channel.toLowerCase()}.requests.${sessionId}`;
238
+
239
+ return new Promise((resolve, reject) => {
240
+ let settled = false;
241
+
242
+ const cleanup = () => {
243
+ settled = true;
244
+ if (typeof unsubscribe === "function") unsubscribe();
245
+ clearTimeout(pollTimer);
246
+ signal.removeEventListener("abort", onAbort);
247
+ };
248
+
249
+ const onAbort = () => {
250
+ cleanup();
251
+ reject(new Error("Aborted"));
252
+ };
253
+ signal.addEventListener("abort", onAbort);
254
+
255
+ const unsubscribe = this.messaging.subscribe<PaymentStatusDto>(
256
+ channelName,
257
+ "payment:status-changed",
258
+ (data: PaymentStatusDto) => {
259
+ if (settled) return;
260
+ if (data.status === SimplePaymentStatus.Pending) return;
261
+ cleanup();
262
+ resolve(this.mapStatusDto(data, request));
263
+ },
264
+ );
265
+
266
+ const pollTimer = setTimeout(() => {
267
+ if (settled) return;
268
+ this.pollUntilTerminal(
269
+ clientToken,
270
+ request,
271
+ signal,
272
+ VivaSessionStrategy.POLLING_DURATION_MS,
273
+ ).then(
274
+ (result) => {
275
+ if (settled) return;
276
+ cleanup();
277
+ resolve(result);
278
+ },
279
+ (error) => {
280
+ if (settled) return;
281
+ cleanup();
282
+ reject(error);
283
+ },
284
+ );
285
+ }, VivaSessionStrategy.INITIAL_POLL_DELAY_MS);
286
+ });
287
+ }
288
+
289
+ private mapStatusDto(
290
+ data: PaymentStatusDto,
291
+ request: PaymentRequest,
292
+ ): PaymentResult {
293
+ const isSuccess = data.status === SimplePaymentStatus.Success;
294
+
295
+ const result: PaymentResult = {
296
+ success: isSuccess,
297
+ status: isSuccess ? SdkPaymentStatus.SUCCESS : SdkPaymentStatus.FAILED,
298
+ orderId: request.orderRef,
299
+ errorCode:
300
+ data.error?.code || (isSuccess ? "" : PaymentFailureCode.SystemUnknown),
301
+ errorMessage:
302
+ data.error?.message ||
303
+ (isSuccess ? "" : "Transaction failed without error details"),
304
+ };
305
+
306
+ if (data.transactionId) {
307
+ result.transactionId = data.transactionId;
308
+ }
309
+ if (data.error?.referenceError) {
310
+ result.errorReference = data.error.referenceError;
311
+ }
312
+ if (data.transaction) {
313
+ result.transaction = data.transaction as unknown as TransactionDto;
314
+ }
315
+
316
+ return result;
317
+ }
318
+
319
+ private finalize(result: PaymentResult): PaymentResult {
320
+ if (result.status !== SdkPaymentStatus.PENDING) {
321
+ this.idempotencyKey = null;
322
+ this.idempotencyOrderRef = null;
323
+ }
324
+ return result;
325
+ }
326
+
327
+ private armCancel(clientToken: string): Promise<void> {
328
+ return this.sessionApi.cancelVivaSession(clientToken).then(
329
+ () => undefined,
330
+ () => undefined,
331
+ );
332
+ }
333
+
334
+ private generateIdempotencyKey(): string {
335
+ const cryptoRef = (
336
+ globalThis as { crypto?: { randomUUID?: () => string } }
337
+ ).crypto;
338
+
339
+ if (cryptoRef?.randomUUID) {
340
+ return cryptoRef.randomUUID();
341
+ }
342
+
343
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
344
+ const random = (Math.random() * 16) | 0;
345
+ const value = char === "x" ? random : (random & 0x3) | 0x8;
346
+ return value.toString(16);
347
+ });
348
+ }
349
+
350
+ private delay(ms: number, signal?: AbortSignal): Promise<void> {
351
+ return new Promise((resolve) => {
352
+ const onAbort = () => {
353
+ clearTimeout(timeout);
354
+ resolve();
355
+ };
356
+ if (signal) {
357
+ signal.addEventListener("abort", onAbort, { once: true });
358
+ }
359
+ const timeout = setTimeout(() => {
360
+ if (signal) {
361
+ signal.removeEventListener("abort", onAbort);
362
+ }
363
+ resolve();
364
+ }, ms);
365
+ });
366
+ }
367
+ }