@munchi_oy/payments 1.10.6 → 1.11.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.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/src/MunchiPaymentSDK.d.ts +1 -0
- package/dist/src/MunchiPaymentSDK.d.ts.map +1 -1
- package/dist/src/strategies/IPaymentStrategy.d.ts +5 -2
- package/dist/src/strategies/IPaymentStrategy.d.ts.map +1 -1
- package/dist/src/strategies/NetsSessionStrategy.d.ts +40 -0
- package/dist/src/strategies/NetsSessionStrategy.d.ts.map +1 -0
- package/dist/src/strategies/VivaSessionStrategy.d.ts +2 -1
- package/dist/src/strategies/VivaSessionStrategy.d.ts.map +1 -1
- package/dist/src/strategies/sessionResult.d.ts +8 -1
- package/dist/src/strategies/sessionResult.d.ts.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/MunchiPaymentSDK.ts +20 -2
- package/src/strategies/IPaymentStrategy.ts +13 -2
- package/src/strategies/NetsSessionStrategy.ts +448 -0
- package/src/strategies/VivaSessionStrategy.ts +11 -5
- package/src/strategies/sessionResult.ts +77 -0
- package/src/version.ts +1 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { type PaymentSessionViewDto } from "@munchi_oy/core";
|
|
1
|
+
import { PaymentSessionStatus, type PaymentSessionViewDto } from "@munchi_oy/core";
|
|
2
2
|
import { type PaymentResult } from "../types/payment";
|
|
3
3
|
export declare function pendingResult(orderId: string): PaymentResult;
|
|
4
4
|
export declare function mapSessionResult(data: PaymentSessionViewDto, orderId: string): PaymentResult;
|
|
5
|
+
export interface ReversalStatusView {
|
|
6
|
+
status: PaymentSessionStatus;
|
|
7
|
+
providerTransactionId?: string;
|
|
8
|
+
errorCode?: string;
|
|
9
|
+
errorMessage?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function mapReversalResult(data: ReversalStatusView, orderId: string): PaymentResult;
|
|
5
12
|
//# sourceMappingURL=sessionResult.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionResult.d.ts","sourceRoot":"","sources":["../../../src/strategies/sessionResult.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"sessionResult.d.ts","sourceRoot":"","sources":["../../../src/strategies/sessionResult.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,oBAAoB,EACpB,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,KAAK,aAAa,EAAoB,MAAM,kBAAkB,CAAC;AASxE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAQ5D;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,qBAAqB,EAC3B,OAAO,EAAE,MAAM,GACd,aAAa,CAgEf;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,MAAM,GACd,aAAa,CAgEf"}
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "1.
|
|
1
|
+
export declare const VERSION = "1.11.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.
|
|
3
|
+
"version": "1.11.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.
|
|
35
|
+
"@munchi_oy/core": "1.9.74"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test": "jest",
|
package/src/MunchiPaymentSDK.ts
CHANGED
|
@@ -4,6 +4,7 @@ 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 { NetsSessionStrategy } from "./strategies/NetsSessionStrategy";
|
|
7
8
|
import { NetsStrategy } from "./strategies/NetsStrategy";
|
|
8
9
|
import { VivaAppToAppStrategy } from "./strategies/VivaAppToAppStrategy";
|
|
9
10
|
import { VivaSessionStrategy } from "./strategies/VivaSessionStrategy";
|
|
@@ -56,6 +57,8 @@ export class MunchiPaymentSDK implements IMunchiPaymentSDK {
|
|
|
56
57
|
...MunchiPaymentSDK.TERMINAL_STATES,
|
|
57
58
|
];
|
|
58
59
|
|
|
60
|
+
private static readonly CANCEL_VERIFY_TIMEOUT_MS = 5000;
|
|
61
|
+
|
|
59
62
|
constructor(
|
|
60
63
|
axios: AxiosInstance,
|
|
61
64
|
messaging: IMessagingAdapter,
|
|
@@ -197,6 +200,9 @@ export class MunchiPaymentSDK implements IMunchiPaymentSDK {
|
|
|
197
200
|
private resolveStrategy(config: PaymentTerminalConfig): IPaymentStrategy {
|
|
198
201
|
switch (config.provider) {
|
|
199
202
|
case PaymentProvider.Nets:
|
|
203
|
+
if (this.paymentSessionConfig?.enabled) {
|
|
204
|
+
return new NetsSessionStrategy(this.axios, this.messaging, config);
|
|
205
|
+
}
|
|
200
206
|
return new NetsStrategy(this.axios, this.messaging, config);
|
|
201
207
|
case PaymentProvider.Worldline:
|
|
202
208
|
return new WorldlineStrategy(this.axios, this.messaging, config);
|
|
@@ -343,10 +349,14 @@ export class MunchiPaymentSDK implements IMunchiPaymentSDK {
|
|
|
343
349
|
|
|
344
350
|
if (this._cancellationIntent) {
|
|
345
351
|
try {
|
|
346
|
-
if (
|
|
352
|
+
if (
|
|
353
|
+
this._currentSessionId &&
|
|
354
|
+
(this.strategy.shouldVerifyAfterCancel?.() ?? true)
|
|
355
|
+
) {
|
|
347
356
|
const finalStatus = await this.strategy.verifyFinalStatus(
|
|
348
357
|
params,
|
|
349
358
|
this._currentSessionId,
|
|
359
|
+
MunchiPaymentSDK.CANCEL_VERIFY_TIMEOUT_MS,
|
|
350
360
|
);
|
|
351
361
|
if (finalStatus.success) {
|
|
352
362
|
this.transitionTo(PaymentInteractionState.SUCCESS);
|
|
@@ -689,7 +699,15 @@ export class MunchiPaymentSDK implements IMunchiPaymentSDK {
|
|
|
689
699
|
this.transitionTo(PaymentInteractionState.CONNECTING);
|
|
690
700
|
|
|
691
701
|
try {
|
|
692
|
-
const result = await revertTransaction(params)
|
|
702
|
+
const result = await revertTransaction(params, (state, detail) => {
|
|
703
|
+
if (detail?.sessionId) {
|
|
704
|
+
this._currentSessionId = detail.sessionId;
|
|
705
|
+
}
|
|
706
|
+
if (state !== PaymentInteractionState.FAILED) {
|
|
707
|
+
this.transitionTo(state);
|
|
708
|
+
this.fireStateCallback(state, callbacks, params.orderRef);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
693
711
|
this.settleSingleShot(result, params.orderRef, callbacks);
|
|
694
712
|
return result;
|
|
695
713
|
} catch (error) {
|
|
@@ -13,6 +13,7 @@ export enum StrategyExecutionMode {
|
|
|
13
13
|
|
|
14
14
|
export interface IPaymentStrategy {
|
|
15
15
|
getExecutionMode?(): StrategyExecutionMode;
|
|
16
|
+
shouldVerifyAfterCancel?(): boolean;
|
|
16
17
|
|
|
17
18
|
processPayment(
|
|
18
19
|
request: PaymentRequest,
|
|
@@ -25,7 +26,17 @@ export interface IPaymentStrategy {
|
|
|
25
26
|
request: RefundRequest,
|
|
26
27
|
onStateChange: (state: PaymentInteractionState, detail?: { sessionId?: string }) => void,
|
|
27
28
|
): Promise<PaymentResult>;
|
|
28
|
-
verifyFinalStatus(
|
|
29
|
-
|
|
29
|
+
verifyFinalStatus(
|
|
30
|
+
request: PaymentRequest,
|
|
31
|
+
sessionId: string,
|
|
32
|
+
timeoutMs?: number,
|
|
33
|
+
): Promise<PaymentResult>;
|
|
34
|
+
revertTransaction?(
|
|
35
|
+
request: RevertRequest,
|
|
36
|
+
onStateChange?: (
|
|
37
|
+
state: PaymentInteractionState,
|
|
38
|
+
detail?: { sessionId?: string },
|
|
39
|
+
) => void,
|
|
40
|
+
): Promise<PaymentResult>;
|
|
30
41
|
abort(): void;
|
|
31
42
|
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type InitiateNetsSessionDto,
|
|
3
|
+
PaymentEventType,
|
|
4
|
+
PaymentSessionApi,
|
|
5
|
+
type PaymentSessionViewDto,
|
|
6
|
+
type PaymentStatusDto,
|
|
7
|
+
type ReverseSessionDto,
|
|
8
|
+
SimplePaymentStatus,
|
|
9
|
+
type TransactionDto,
|
|
10
|
+
} from "@munchi_oy/core";
|
|
11
|
+
import type { AxiosInstance } from "axios";
|
|
12
|
+
|
|
13
|
+
import { PaymentErrorCode, PaymentSDKError } from "../error";
|
|
14
|
+
import {
|
|
15
|
+
type IMessagingAdapter,
|
|
16
|
+
PaymentInteractionState as InteractionState,
|
|
17
|
+
type PaymentInteractionState,
|
|
18
|
+
type PaymentRequest,
|
|
19
|
+
type PaymentResult,
|
|
20
|
+
type PaymentTerminalConfig,
|
|
21
|
+
type RefundRequest,
|
|
22
|
+
type RevertRequest,
|
|
23
|
+
SdkPaymentStatus,
|
|
24
|
+
} from "../types/payment";
|
|
25
|
+
import type { IPaymentStrategy } from "./IPaymentStrategy";
|
|
26
|
+
import {
|
|
27
|
+
mapReversalResult,
|
|
28
|
+
mapSessionResult,
|
|
29
|
+
pendingResult,
|
|
30
|
+
type ReversalStatusView,
|
|
31
|
+
} from "./sessionResult";
|
|
32
|
+
|
|
33
|
+
export class NetsSessionStrategy implements IPaymentStrategy {
|
|
34
|
+
private static readonly POLLING_DURATION_MS = 180000;
|
|
35
|
+
private static readonly POLLING_INTERVAL_MS = 1000;
|
|
36
|
+
private static readonly VERIFY_DURATION_MS = 30000;
|
|
37
|
+
private static readonly INITIAL_POLL_DELAY_MS = 10000;
|
|
38
|
+
|
|
39
|
+
private sessionApi: PaymentSessionApi;
|
|
40
|
+
private abortController: AbortController | null = null;
|
|
41
|
+
private idempotencyKey: string | null = null;
|
|
42
|
+
private idempotencyOrderRef: string | null = null;
|
|
43
|
+
private currentClientToken: string | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
axios: AxiosInstance,
|
|
47
|
+
private messaging: IMessagingAdapter,
|
|
48
|
+
private config: PaymentTerminalConfig,
|
|
49
|
+
) {
|
|
50
|
+
this.sessionApi = new PaymentSessionApi(undefined, "", axios);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async processPayment(
|
|
54
|
+
request: PaymentRequest,
|
|
55
|
+
onStateChange: (
|
|
56
|
+
state: PaymentInteractionState,
|
|
57
|
+
detail?: { sessionId?: string },
|
|
58
|
+
) => void,
|
|
59
|
+
): Promise<PaymentResult> {
|
|
60
|
+
this.abortController = new AbortController();
|
|
61
|
+
if (
|
|
62
|
+
this.idempotencyKey === null ||
|
|
63
|
+
this.idempotencyOrderRef !== request.orderRef
|
|
64
|
+
) {
|
|
65
|
+
this.idempotencyKey = this.generateIdempotencyKey();
|
|
66
|
+
this.idempotencyOrderRef = request.orderRef;
|
|
67
|
+
}
|
|
68
|
+
this.currentClientToken = null;
|
|
69
|
+
|
|
70
|
+
onStateChange(InteractionState.CONNECTING);
|
|
71
|
+
|
|
72
|
+
const payload: InitiateNetsSessionDto = {
|
|
73
|
+
amount: request.amountCents,
|
|
74
|
+
businessId: Number(this.config.storeId),
|
|
75
|
+
orderId: request.orderRef,
|
|
76
|
+
};
|
|
77
|
+
if (request.splitContext !== undefined) {
|
|
78
|
+
payload.splitContext = request.splitContext;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const { data } = await this.sessionApi.initiateNetsSession(
|
|
83
|
+
payload,
|
|
84
|
+
this.idempotencyKey,
|
|
85
|
+
);
|
|
86
|
+
this.currentClientToken = data.clientToken;
|
|
87
|
+
const signal = this.abortController.signal;
|
|
88
|
+
|
|
89
|
+
if (signal.aborted) {
|
|
90
|
+
throw new Error("Aborted");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onStateChange(InteractionState.REQUIRES_INPUT, {
|
|
94
|
+
sessionId: data.sessionId,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const terminal = await this.waitForCompletion(
|
|
98
|
+
data.clientToken,
|
|
99
|
+
data.sessionId,
|
|
100
|
+
request,
|
|
101
|
+
signal,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (terminal) {
|
|
105
|
+
return this.finalize(terminal);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await this.armCancel(data.clientToken);
|
|
109
|
+
return pendingResult(request.orderRef);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err instanceof PaymentSDKError) throw err;
|
|
112
|
+
throw new PaymentSDKError(
|
|
113
|
+
PaymentErrorCode.TERMINAL_BUSY,
|
|
114
|
+
"Failed to create Nets payment session",
|
|
115
|
+
err,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async verifyFinalStatus(
|
|
121
|
+
request: PaymentRequest,
|
|
122
|
+
_sessionId: string,
|
|
123
|
+
timeoutMs?: number,
|
|
124
|
+
): Promise<PaymentResult> {
|
|
125
|
+
if (!this.currentClientToken) {
|
|
126
|
+
throw new PaymentSDKError(
|
|
127
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
128
|
+
"No active payment session to verify",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const terminal = await this.pollUntilTerminal(
|
|
133
|
+
this.currentClientToken,
|
|
134
|
+
request.orderRef,
|
|
135
|
+
undefined,
|
|
136
|
+
timeoutMs ?? NetsSessionStrategy.VERIFY_DURATION_MS,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return this.finalize(terminal ?? pendingResult(request.orderRef));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async cancelTransaction(
|
|
143
|
+
_onStateChange: (state: PaymentInteractionState) => void,
|
|
144
|
+
): Promise<boolean> {
|
|
145
|
+
if (!this.currentClientToken) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await this.sessionApi.cancelSession(this.currentClientToken);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new PaymentSDKError(
|
|
153
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
154
|
+
"Failed to cancel Nets payment session",
|
|
155
|
+
error,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.abortController?.abort();
|
|
160
|
+
this.resetIdempotencyKey();
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async refundTransaction(
|
|
165
|
+
request: RefundRequest,
|
|
166
|
+
onStateChange: (
|
|
167
|
+
state: PaymentInteractionState,
|
|
168
|
+
detail?: { sessionId?: string },
|
|
169
|
+
) => void,
|
|
170
|
+
): Promise<PaymentResult> {
|
|
171
|
+
this.abortController = new AbortController();
|
|
172
|
+
onStateChange(InteractionState.CONNECTING);
|
|
173
|
+
|
|
174
|
+
const payload: ReverseSessionDto = {
|
|
175
|
+
sessionId: request.originalTransactionId,
|
|
176
|
+
amount: request.amountCents,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const { data } = await this.sessionApi.refundNetsSession(payload);
|
|
181
|
+
return await this.settleReversal(
|
|
182
|
+
data,
|
|
183
|
+
request.orderRef,
|
|
184
|
+
this.abortController.signal,
|
|
185
|
+
true,
|
|
186
|
+
onStateChange,
|
|
187
|
+
);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof PaymentSDKError) throw error;
|
|
190
|
+
throw new PaymentSDKError(
|
|
191
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
192
|
+
"Failed to refund Nets transaction",
|
|
193
|
+
error,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async revertTransaction(
|
|
199
|
+
request: RevertRequest,
|
|
200
|
+
onStateChange?: (
|
|
201
|
+
state: PaymentInteractionState,
|
|
202
|
+
detail?: { sessionId?: string },
|
|
203
|
+
) => void,
|
|
204
|
+
): Promise<PaymentResult> {
|
|
205
|
+
this.abortController = new AbortController();
|
|
206
|
+
|
|
207
|
+
const payload: ReverseSessionDto = {
|
|
208
|
+
sessionId: request.originalTransactionId,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const { data } = await this.sessionApi.revertNetsSession(payload);
|
|
213
|
+
return await this.settleReversal(
|
|
214
|
+
data,
|
|
215
|
+
request.orderRef,
|
|
216
|
+
this.abortController.signal,
|
|
217
|
+
data.requiresCardTap,
|
|
218
|
+
onStateChange,
|
|
219
|
+
);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error instanceof PaymentSDKError) throw error;
|
|
222
|
+
throw new PaymentSDKError(
|
|
223
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
224
|
+
"Failed to revert Nets transaction",
|
|
225
|
+
error,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
shouldVerifyAfterCancel(): boolean {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
abort(): void {
|
|
235
|
+
this.abortController?.abort();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private async settleReversal(
|
|
239
|
+
view: ReversalStatusView & { clientToken: string; sessionId: string },
|
|
240
|
+
orderRef: string,
|
|
241
|
+
signal: AbortSignal,
|
|
242
|
+
requiresCardTap: boolean,
|
|
243
|
+
onStateChange?: (
|
|
244
|
+
state: PaymentInteractionState,
|
|
245
|
+
detail?: { sessionId?: string },
|
|
246
|
+
) => void,
|
|
247
|
+
): Promise<PaymentResult> {
|
|
248
|
+
const immediate = mapReversalResult(view, orderRef);
|
|
249
|
+
if (immediate.status !== SdkPaymentStatus.PENDING) {
|
|
250
|
+
return immediate;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (requiresCardTap) {
|
|
254
|
+
onStateChange?.(InteractionState.REQUIRES_INPUT, {
|
|
255
|
+
sessionId: view.sessionId,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const terminal = await this.pollUntilTerminal(
|
|
260
|
+
view.clientToken,
|
|
261
|
+
orderRef,
|
|
262
|
+
signal,
|
|
263
|
+
NetsSessionStrategy.POLLING_DURATION_MS,
|
|
264
|
+
mapReversalResult,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return terminal ?? pendingResult(orderRef);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private async pollUntilTerminal(
|
|
271
|
+
clientToken: string,
|
|
272
|
+
orderRef: string,
|
|
273
|
+
signal: AbortSignal | undefined,
|
|
274
|
+
durationMs: number,
|
|
275
|
+
map: (
|
|
276
|
+
data: PaymentSessionViewDto,
|
|
277
|
+
orderId: string,
|
|
278
|
+
) => PaymentResult = mapSessionResult,
|
|
279
|
+
): Promise<PaymentResult | undefined> {
|
|
280
|
+
const deadline = Date.now() + durationMs;
|
|
281
|
+
|
|
282
|
+
while (Date.now() < deadline) {
|
|
283
|
+
if (signal?.aborted) {
|
|
284
|
+
throw new Error("Aborted");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let data: PaymentSessionViewDto | undefined;
|
|
288
|
+
try {
|
|
289
|
+
data = (await this.sessionApi.getPaymentSession(clientToken)).data;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error instanceof Error && error.message === "Aborted") throw error;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (signal?.aborted) {
|
|
295
|
+
throw new Error("Aborted");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (data) {
|
|
299
|
+
const result = map(data, orderRef);
|
|
300
|
+
if (result.status !== SdkPaymentStatus.PENDING) {
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await this.delay(NetsSessionStrategy.POLLING_INTERVAL_MS, signal);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private waitForCompletion(
|
|
312
|
+
clientToken: string,
|
|
313
|
+
sessionId: string,
|
|
314
|
+
request: PaymentRequest,
|
|
315
|
+
signal: AbortSignal,
|
|
316
|
+
): Promise<PaymentResult | undefined> {
|
|
317
|
+
const channelName = `nets.requests.${sessionId}`;
|
|
318
|
+
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
let settled = false;
|
|
321
|
+
|
|
322
|
+
const cleanup = () => {
|
|
323
|
+
settled = true;
|
|
324
|
+
if (typeof unsubscribe === "function") unsubscribe();
|
|
325
|
+
clearTimeout(pollTimer);
|
|
326
|
+
signal.removeEventListener("abort", onAbort);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const onAbort = () => {
|
|
330
|
+
cleanup();
|
|
331
|
+
reject(new Error("Aborted"));
|
|
332
|
+
};
|
|
333
|
+
signal.addEventListener("abort", onAbort);
|
|
334
|
+
|
|
335
|
+
const unsubscribe = this.messaging.subscribe<PaymentStatusDto>(
|
|
336
|
+
channelName,
|
|
337
|
+
PaymentEventType.StatusChanged,
|
|
338
|
+
(data: PaymentStatusDto) => {
|
|
339
|
+
if (settled) return;
|
|
340
|
+
if (data.status === SimplePaymentStatus.Pending) return;
|
|
341
|
+
cleanup();
|
|
342
|
+
resolve(this.mapStatusDto(data, request));
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const pollTimer = setTimeout(() => {
|
|
347
|
+
if (settled) return;
|
|
348
|
+
this.pollUntilTerminal(
|
|
349
|
+
clientToken,
|
|
350
|
+
request.orderRef,
|
|
351
|
+
signal,
|
|
352
|
+
NetsSessionStrategy.POLLING_DURATION_MS,
|
|
353
|
+
).then(
|
|
354
|
+
(result) => {
|
|
355
|
+
if (settled) return;
|
|
356
|
+
cleanup();
|
|
357
|
+
resolve(result);
|
|
358
|
+
},
|
|
359
|
+
(error) => {
|
|
360
|
+
if (settled) return;
|
|
361
|
+
cleanup();
|
|
362
|
+
reject(error);
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
}, NetsSessionStrategy.INITIAL_POLL_DELAY_MS);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private mapStatusDto(
|
|
370
|
+
data: PaymentStatusDto,
|
|
371
|
+
request: PaymentRequest,
|
|
372
|
+
): PaymentResult {
|
|
373
|
+
const isSuccess = data.status === SimplePaymentStatus.Success;
|
|
374
|
+
|
|
375
|
+
const result: PaymentResult = {
|
|
376
|
+
success: isSuccess,
|
|
377
|
+
status: isSuccess ? SdkPaymentStatus.SUCCESS : SdkPaymentStatus.FAILED,
|
|
378
|
+
orderId: request.orderRef,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
if (data.transactionId) {
|
|
382
|
+
result.transactionId = data.transactionId;
|
|
383
|
+
}
|
|
384
|
+
if (data.transaction) {
|
|
385
|
+
result.transaction = data.transaction as unknown as TransactionDto;
|
|
386
|
+
}
|
|
387
|
+
if (data.error?.code) {
|
|
388
|
+
result.errorCode = data.error.code;
|
|
389
|
+
}
|
|
390
|
+
if (data.error?.message) {
|
|
391
|
+
result.errorMessage = data.error.message;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private finalize(result: PaymentResult): PaymentResult {
|
|
398
|
+
if (result.status !== SdkPaymentStatus.PENDING) {
|
|
399
|
+
this.resetIdempotencyKey();
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private resetIdempotencyKey(): void {
|
|
405
|
+
this.idempotencyKey = null;
|
|
406
|
+
this.idempotencyOrderRef = null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private armCancel(clientToken: string): Promise<void> {
|
|
410
|
+
return this.sessionApi.cancelSession(clientToken).then(
|
|
411
|
+
() => undefined,
|
|
412
|
+
() => undefined,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private generateIdempotencyKey(): string {
|
|
417
|
+
const cryptoRef = (globalThis as { crypto?: { randomUUID?: () => string } })
|
|
418
|
+
.crypto;
|
|
419
|
+
|
|
420
|
+
if (cryptoRef?.randomUUID) {
|
|
421
|
+
return cryptoRef.randomUUID();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
|
|
425
|
+
const random = (Math.random() * 16) | 0;
|
|
426
|
+
const value = char === "x" ? random : (random & 0x3) | 0x8;
|
|
427
|
+
return value.toString(16);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
432
|
+
return new Promise((resolve) => {
|
|
433
|
+
const onAbort = () => {
|
|
434
|
+
clearTimeout(timeout);
|
|
435
|
+
resolve();
|
|
436
|
+
};
|
|
437
|
+
if (signal) {
|
|
438
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
439
|
+
}
|
|
440
|
+
const timeout = setTimeout(() => {
|
|
441
|
+
if (signal) {
|
|
442
|
+
signal.removeEventListener("abort", onAbort);
|
|
443
|
+
}
|
|
444
|
+
resolve();
|
|
445
|
+
}, ms);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -111,6 +111,7 @@ export class VivaSessionStrategy implements IPaymentStrategy {
|
|
|
111
111
|
async verifyFinalStatus(
|
|
112
112
|
request: PaymentRequest,
|
|
113
113
|
_sessionId: string,
|
|
114
|
+
timeoutMs?: number,
|
|
114
115
|
): Promise<PaymentResult> {
|
|
115
116
|
if (!this.currentClientToken) {
|
|
116
117
|
throw new PaymentSDKError(
|
|
@@ -123,7 +124,7 @@ export class VivaSessionStrategy implements IPaymentStrategy {
|
|
|
123
124
|
this.currentClientToken,
|
|
124
125
|
request,
|
|
125
126
|
undefined,
|
|
126
|
-
VivaSessionStrategy.VERIFY_DURATION_MS,
|
|
127
|
+
timeoutMs ?? VivaSessionStrategy.VERIFY_DURATION_MS,
|
|
127
128
|
);
|
|
128
129
|
|
|
129
130
|
return this.finalize(terminal ?? pendingResult(request.orderRef));
|
|
@@ -137,7 +138,7 @@ export class VivaSessionStrategy implements IPaymentStrategy {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
try {
|
|
140
|
-
await this.sessionApi.
|
|
141
|
+
await this.sessionApi.cancelSession(this.currentClientToken);
|
|
141
142
|
} catch (error) {
|
|
142
143
|
throw new PaymentSDKError(
|
|
143
144
|
PaymentErrorCode.NETWORK_ERROR,
|
|
@@ -147,6 +148,7 @@ export class VivaSessionStrategy implements IPaymentStrategy {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
this.abortController?.abort();
|
|
151
|
+
this.resetIdempotencyKey();
|
|
150
152
|
return true;
|
|
151
153
|
}
|
|
152
154
|
|
|
@@ -353,14 +355,18 @@ export class VivaSessionStrategy implements IPaymentStrategy {
|
|
|
353
355
|
|
|
354
356
|
private finalize(result: PaymentResult): PaymentResult {
|
|
355
357
|
if (result.status !== SdkPaymentStatus.PENDING) {
|
|
356
|
-
this.
|
|
357
|
-
this.idempotencyOrderRef = null;
|
|
358
|
+
this.resetIdempotencyKey();
|
|
358
359
|
}
|
|
359
360
|
return result;
|
|
360
361
|
}
|
|
361
362
|
|
|
363
|
+
private resetIdempotencyKey(): void {
|
|
364
|
+
this.idempotencyKey = null;
|
|
365
|
+
this.idempotencyOrderRef = null;
|
|
366
|
+
}
|
|
367
|
+
|
|
362
368
|
private armCancel(clientToken: string): Promise<void> {
|
|
363
|
-
return this.sessionApi.
|
|
369
|
+
return this.sessionApi.cancelSession(clientToken).then(
|
|
364
370
|
() => undefined,
|
|
365
371
|
() => undefined,
|
|
366
372
|
);
|