@bloonio/lokotro-pay 1.1.0 → 1.1.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.
|
@@ -2,9 +2,9 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { Injectable, EventEmitter, Output, Input, Component, forwardRef, InjectionToken, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1$1 from '@angular/forms';
|
|
4
4
|
import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS, Validators } from '@angular/forms';
|
|
5
|
-
import { BehaviorSubject, of,
|
|
6
|
-
import { UpperCasePipe } from '@angular/common';
|
|
5
|
+
import { BehaviorSubject, of, Subject, throwError, interval } from 'rxjs';
|
|
7
6
|
import { timeout, map, catchError, tap, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
|
|
7
|
+
import { UpperCasePipe } from '@angular/common';
|
|
8
8
|
import * as i1 from '@angular/common/http';
|
|
9
9
|
import { HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
|
10
10
|
|
|
@@ -17,8 +17,7 @@ import { HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
|
|
17
17
|
*/
|
|
18
18
|
const LOKOTRO_DEV_ENV = {
|
|
19
19
|
environment: 'development',
|
|
20
|
-
apiBaseUrl: '
|
|
21
|
-
// apiBaseUrl: 'https://dev.app.api.gtwy.lokotro.com',
|
|
20
|
+
apiBaseUrl: 'https://dev.app.api.gtwy.lokotro.com',
|
|
22
21
|
paymentApiVersion: 'v1',
|
|
23
22
|
debugMode: true,
|
|
24
23
|
logLevel: 'debug',
|
|
@@ -1253,7 +1252,6 @@ class LokotroHttpClientService {
|
|
|
1253
1252
|
this.acceptLanguage = 'fr';
|
|
1254
1253
|
this.customHeaders = {};
|
|
1255
1254
|
this.instanceId = ++LokotroHttpClientService.instanceCounter;
|
|
1256
|
-
console.log(`[Lokotro HTTP] Instance #${this.instanceId} created`);
|
|
1257
1255
|
}
|
|
1258
1256
|
/**
|
|
1259
1257
|
* Configure the HTTP client
|
|
@@ -1273,9 +1271,7 @@ class LokotroHttpClientService {
|
|
|
1273
1271
|
* Set app-key for Lokotro Gateway authentication
|
|
1274
1272
|
*/
|
|
1275
1273
|
setAppKey(appKey) {
|
|
1276
|
-
console.log(`[Lokotro HTTP #${this.instanceId}] setAppKey called with:`, appKey?.substring(0, 20) + '...');
|
|
1277
1274
|
this.appKey = appKey;
|
|
1278
|
-
console.log(`[Lokotro HTTP #${this.instanceId}] appKey now set to:`, this.appKey?.substring(0, 20) + '...');
|
|
1279
1275
|
}
|
|
1280
1276
|
/**
|
|
1281
1277
|
* Remove app-key
|
|
@@ -1299,7 +1295,6 @@ class LokotroHttpClientService {
|
|
|
1299
1295
|
* Build request headers
|
|
1300
1296
|
*/
|
|
1301
1297
|
buildHeaders() {
|
|
1302
|
-
console.log(`[Lokotro HTTP #${this.instanceId}] buildHeaders - appKey value:`, this.appKey?.substring(0, 20) + '...');
|
|
1303
1298
|
let headers = new HttpHeaders({
|
|
1304
1299
|
'Content-Type': 'application/json',
|
|
1305
1300
|
'Accept': 'application/json',
|
|
@@ -1309,10 +1304,9 @@ class LokotroHttpClientService {
|
|
|
1309
1304
|
});
|
|
1310
1305
|
if (this.appKey) {
|
|
1311
1306
|
headers = headers.set('app-key', this.appKey);
|
|
1312
|
-
console.log(`[Lokotro HTTP #${this.instanceId}] app-key header added`);
|
|
1313
1307
|
}
|
|
1314
|
-
else {
|
|
1315
|
-
console.warn(
|
|
1308
|
+
else if (LokotroPayEnv.debugMode) {
|
|
1309
|
+
console.warn('[Lokotro HTTP] appKey is not set');
|
|
1316
1310
|
}
|
|
1317
1311
|
// Add custom headers
|
|
1318
1312
|
Object.entries(this.customHeaders).forEach(([key, value]) => {
|
|
@@ -3370,6 +3364,16 @@ const initialState = {
|
|
|
3370
3364
|
currentScreen: LokotroPayScreenNavigation.LoadingScreen
|
|
3371
3365
|
};
|
|
3372
3366
|
class LokotroPaymentService {
|
|
3367
|
+
/** Allowed domains for redirect URLs to prevent open redirect attacks */
|
|
3368
|
+
static { this.ALLOWED_REDIRECT_DOMAINS = [
|
|
3369
|
+
'lokotro.com',
|
|
3370
|
+
'app.lokotro.com',
|
|
3371
|
+
'api.gtwy.lokotro.com',
|
|
3372
|
+
'dev.app.api.gtwy.lokotro.com',
|
|
3373
|
+
'app.api.gtwy.lokotro.com',
|
|
3374
|
+
'mastercard.com',
|
|
3375
|
+
'ap-gateway.mastercard.com',
|
|
3376
|
+
]; }
|
|
3373
3377
|
static { this.MOBILE_MONEY_POLL_INTERVAL = 5000; }
|
|
3374
3378
|
static { this.MOBILE_MONEY_MAX_ATTEMPTS = 60; }
|
|
3375
3379
|
constructor(httpClient) {
|
|
@@ -3377,6 +3381,7 @@ class LokotroPaymentService {
|
|
|
3377
3381
|
this.stateSubject = new BehaviorSubject(initialState);
|
|
3378
3382
|
this.state$ = this.stateSubject.asObservable();
|
|
3379
3383
|
this.mobileMoneyPollingAttempts = 0;
|
|
3384
|
+
this.pollingStop$ = new Subject();
|
|
3380
3385
|
}
|
|
3381
3386
|
/**
|
|
3382
3387
|
* Get current state
|
|
@@ -3421,9 +3426,7 @@ class LokotroPaymentService {
|
|
|
3421
3426
|
this.currentPaymentBody = paymentBody;
|
|
3422
3427
|
this.updateState({ isLoading: true });
|
|
3423
3428
|
const requestData = this.convertPaymentBodyToRequest(paymentBody);
|
|
3424
|
-
console.log('[Lokotro Payment] Creating payment with body:', requestData);
|
|
3425
3429
|
return this.httpClient.post(LokotroPayEnv.endpoints.collect, requestData).pipe(switchMap(response => {
|
|
3426
|
-
console.log('[Lokotro Payment] Create payment response:', response);
|
|
3427
3430
|
if (!response.isSuccess || !response.data) {
|
|
3428
3431
|
this.updateState({
|
|
3429
3432
|
isLoading: false,
|
|
@@ -3481,6 +3484,10 @@ class LokotroPaymentService {
|
|
|
3481
3484
|
return;
|
|
3482
3485
|
}
|
|
3483
3486
|
if (submitResponse.redirectUrl) {
|
|
3487
|
+
if (!this.isValidRedirectUrl(submitResponse.redirectUrl)) {
|
|
3488
|
+
this.handlePaymentFailure('Invalid redirect URL received from server');
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3484
3491
|
this.updateState({
|
|
3485
3492
|
isLoading: false,
|
|
3486
3493
|
transactionId: submitResponse.transactionId || this.resolvePaymentId(request)
|
|
@@ -3591,34 +3598,37 @@ class LokotroPaymentService {
|
|
|
3591
3598
|
startMobileMoneyPolling(transactionId) {
|
|
3592
3599
|
this.stopMobileMoneyPolling();
|
|
3593
3600
|
this.mobileMoneyPollingAttempts = 0;
|
|
3594
|
-
|
|
3601
|
+
this.pollingStop$ = new Subject();
|
|
3595
3602
|
this.mobileMoneyPollingTimer = setInterval(() => {
|
|
3596
3603
|
this.mobileMoneyPollingAttempts++;
|
|
3597
3604
|
if (this.mobileMoneyPollingAttempts > LokotroPaymentService.MOBILE_MONEY_MAX_ATTEMPTS) {
|
|
3598
|
-
console.warn('[Lokotro Payment] Mobile money polling max attempts reached');
|
|
3599
3605
|
this.stopMobileMoneyPolling();
|
|
3600
3606
|
this.handlePaymentFailure('Payment took too long. Please try again or contact support.');
|
|
3601
3607
|
return;
|
|
3602
3608
|
}
|
|
3603
3609
|
const endpoint = `${LokotroPayEnv.endpoints.mobileMoneyStatus}/${transactionId}`;
|
|
3604
|
-
this.httpClient.get(endpoint)
|
|
3610
|
+
this.pollingSubscription = this.httpClient.get(endpoint)
|
|
3611
|
+
.pipe(takeUntil(this.pollingStop$))
|
|
3612
|
+
.subscribe({
|
|
3605
3613
|
next: (response) => {
|
|
3606
3614
|
if (!response.isSuccess || !response.data)
|
|
3607
3615
|
return;
|
|
3608
3616
|
const data = this.unwrapPayload(response.data);
|
|
3609
3617
|
const status = this.getString(data['status']).toLowerCase();
|
|
3610
|
-
|
|
3611
|
-
if (
|
|
3618
|
+
const parsedStatus = LokotroPaymentStatusInfo.fromString(status);
|
|
3619
|
+
if (LokotroPaymentStatusInfo.isSuccess(parsedStatus)) {
|
|
3612
3620
|
this.stopMobileMoneyPolling();
|
|
3613
3621
|
this.handlePaymentSuccess(data);
|
|
3614
3622
|
}
|
|
3615
|
-
else if (
|
|
3623
|
+
else if (LokotroPaymentStatusInfo.isFailure(parsedStatus)) {
|
|
3616
3624
|
this.stopMobileMoneyPolling();
|
|
3617
3625
|
this.handlePaymentFailure(this.getString(data['message']) || 'Payment failed or was declined');
|
|
3618
3626
|
}
|
|
3619
3627
|
},
|
|
3620
3628
|
error: (err) => {
|
|
3621
|
-
|
|
3629
|
+
if (LokotroPayEnv.debugMode) {
|
|
3630
|
+
console.error('[Lokotro Payment] Mobile money polling error:', err?.message || err);
|
|
3631
|
+
}
|
|
3622
3632
|
}
|
|
3623
3633
|
});
|
|
3624
3634
|
}, LokotroPaymentService.MOBILE_MONEY_POLL_INTERVAL);
|
|
@@ -3627,11 +3637,13 @@ class LokotroPaymentService {
|
|
|
3627
3637
|
* Stop mobile money status polling
|
|
3628
3638
|
*/
|
|
3629
3639
|
stopMobileMoneyPolling() {
|
|
3640
|
+
this.pollingStop$.next();
|
|
3641
|
+
this.pollingSubscription?.unsubscribe();
|
|
3642
|
+
this.pollingSubscription = undefined;
|
|
3630
3643
|
if (this.mobileMoneyPollingTimer) {
|
|
3631
3644
|
clearInterval(this.mobileMoneyPollingTimer);
|
|
3632
3645
|
this.mobileMoneyPollingTimer = undefined;
|
|
3633
3646
|
this.mobileMoneyPollingAttempts = 0;
|
|
3634
|
-
console.log('[Lokotro Payment] Mobile money polling stopped');
|
|
3635
3647
|
}
|
|
3636
3648
|
}
|
|
3637
3649
|
/**
|
|
@@ -3707,6 +3719,10 @@ class LokotroPaymentService {
|
|
|
3707
3719
|
this.handlePaymentFailure('Hosted session created but no checkout URL was provided');
|
|
3708
3720
|
return of(void 0);
|
|
3709
3721
|
}
|
|
3722
|
+
if (!this.isValidRedirectUrl(redirectUrl)) {
|
|
3723
|
+
this.handlePaymentFailure('Invalid checkout redirect URL received from server');
|
|
3724
|
+
return of(void 0);
|
|
3725
|
+
}
|
|
3710
3726
|
this.updateState({
|
|
3711
3727
|
isLoading: false,
|
|
3712
3728
|
transactionId: paymentId
|
|
@@ -3759,6 +3775,10 @@ class LokotroPaymentService {
|
|
|
3759
3775
|
const hostedCheckoutUrl = this.getString(transactionData['mastercardUrl']) ||
|
|
3760
3776
|
this.getString(transactionData['redirect_url']);
|
|
3761
3777
|
if (hostedCheckoutUrl) {
|
|
3778
|
+
if (!this.isValidRedirectUrl(hostedCheckoutUrl)) {
|
|
3779
|
+
this.handlePaymentFailure('Invalid hosted checkout URL received from server');
|
|
3780
|
+
return of(void 0);
|
|
3781
|
+
}
|
|
3762
3782
|
this.updateState({ isLoading: false });
|
|
3763
3783
|
window.location.href = hostedCheckoutUrl;
|
|
3764
3784
|
return of(void 0);
|
|
@@ -3862,6 +3882,9 @@ class LokotroPaymentService {
|
|
|
3862
3882
|
if (body.mastercardPaymentMethod) {
|
|
3863
3883
|
request['mastercard_payment_method'] = body.mastercardPaymentMethod;
|
|
3864
3884
|
}
|
|
3885
|
+
if (body.transactionalCurrency) {
|
|
3886
|
+
request['transactional_currency'] = body.transactionalCurrency.toLowerCase();
|
|
3887
|
+
}
|
|
3865
3888
|
if (body.metadata) {
|
|
3866
3889
|
request['metadata'] = body.metadata;
|
|
3867
3890
|
}
|
|
@@ -4129,6 +4152,32 @@ class LokotroPaymentService {
|
|
|
4129
4152
|
.map(item => this.asRecord(item))
|
|
4130
4153
|
.filter((item) => Boolean(item));
|
|
4131
4154
|
}
|
|
4155
|
+
/**
|
|
4156
|
+
* Validate that a redirect URL points to an allowed domain.
|
|
4157
|
+
* Prevents open redirect attacks if the API response is compromised.
|
|
4158
|
+
*/
|
|
4159
|
+
isValidRedirectUrl(url) {
|
|
4160
|
+
try {
|
|
4161
|
+
const parsed = new URL(url);
|
|
4162
|
+
if (parsed.protocol !== 'https:') {
|
|
4163
|
+
return false;
|
|
4164
|
+
}
|
|
4165
|
+
return LokotroPaymentService.ALLOWED_REDIRECT_DOMAINS.some(domain => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`));
|
|
4166
|
+
}
|
|
4167
|
+
catch {
|
|
4168
|
+
return false;
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
/**
|
|
4172
|
+
* Full cleanup - call when the host component is destroyed.
|
|
4173
|
+
* Addresses singleton state collision by ensuring polling and state are reset.
|
|
4174
|
+
*/
|
|
4175
|
+
destroy() {
|
|
4176
|
+
this.stopMobileMoneyPolling();
|
|
4177
|
+
this.pollingStop$.complete();
|
|
4178
|
+
this.currentPaymentBody = undefined;
|
|
4179
|
+
this.stateSubject.next(initialState);
|
|
4180
|
+
}
|
|
4132
4181
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: LokotroPaymentService, deps: [{ token: LokotroHttpClientService }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4133
4182
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: LokotroPaymentService, providedIn: 'root' }); }
|
|
4134
4183
|
}
|
|
@@ -4155,6 +4204,7 @@ class LokotroPayCheckoutComponent {
|
|
|
4155
4204
|
this.closed = new EventEmitter();
|
|
4156
4205
|
this.isDarkTheme = true;
|
|
4157
4206
|
this.retryAttempts = 0;
|
|
4207
|
+
this.destroy$ = new Subject();
|
|
4158
4208
|
}
|
|
4159
4209
|
/** Check if user has retries left */
|
|
4160
4210
|
get hasRetriesLeft() {
|
|
@@ -4167,22 +4217,17 @@ class LokotroPayCheckoutComponent {
|
|
|
4167
4217
|
this.initializePayment();
|
|
4168
4218
|
}
|
|
4169
4219
|
ngOnDestroy() {
|
|
4220
|
+
this.destroy$.next();
|
|
4221
|
+
this.destroy$.complete();
|
|
4170
4222
|
this.stateSubscription?.unsubscribe();
|
|
4171
|
-
this.paymentService.
|
|
4223
|
+
this.paymentService.destroy();
|
|
4172
4224
|
}
|
|
4173
4225
|
initializeEnvironment() {
|
|
4174
|
-
console.log('[Lokotro Checkout] initializeEnvironment - configs:', {
|
|
4175
|
-
token: this.configs.token?.substring(0, 20) + '...',
|
|
4176
|
-
isProduction: this.configs.isProduction,
|
|
4177
|
-
customApiUrl: this.configs.customApiUrl
|
|
4178
|
-
});
|
|
4179
4226
|
LokotroPayEnv.initialize(this.configs.isProduction ?? false, this.configs.customApiUrl);
|
|
4180
|
-
console.log('[Lokotro Checkout] Calling setAppKey with token');
|
|
4181
4227
|
this.paymentService.setAppKey(this.configs.token);
|
|
4182
4228
|
if (this.configs.acceptLanguage) {
|
|
4183
4229
|
this.paymentService.setAcceptLanguage(this.configs.acceptLanguage);
|
|
4184
4230
|
}
|
|
4185
|
-
console.log('[Lokotro Checkout] Environment initialized');
|
|
4186
4231
|
}
|
|
4187
4232
|
initializeLocalization() {
|
|
4188
4233
|
if (this.language) {
|
|
@@ -4221,9 +4266,13 @@ class LokotroPayCheckoutComponent {
|
|
|
4221
4266
|
});
|
|
4222
4267
|
}
|
|
4223
4268
|
initializePayment() {
|
|
4224
|
-
this.paymentService.createPayment(this.paymentBody)
|
|
4269
|
+
this.paymentService.createPayment(this.paymentBody)
|
|
4270
|
+
.pipe(takeUntil(this.destroy$))
|
|
4271
|
+
.subscribe({
|
|
4225
4272
|
error: (err) => {
|
|
4226
|
-
|
|
4273
|
+
if (LokotroPayEnv.debugMode) {
|
|
4274
|
+
console.error('[Lokotro Pay] Payment initialization error:', err?.message || err);
|
|
4275
|
+
}
|
|
4227
4276
|
}
|
|
4228
4277
|
});
|
|
4229
4278
|
}
|
|
@@ -4289,9 +4338,13 @@ class LokotroPayCheckoutComponent {
|
|
|
4289
4338
|
paymentMethod: this.state?.selectedPaymentMethod?.name || '',
|
|
4290
4339
|
...formData
|
|
4291
4340
|
};
|
|
4292
|
-
this.paymentService.submitPaymentDetails(submitRequest)
|
|
4341
|
+
this.paymentService.submitPaymentDetails(submitRequest)
|
|
4342
|
+
.pipe(takeUntil(this.destroy$))
|
|
4343
|
+
.subscribe({
|
|
4293
4344
|
error: (err) => {
|
|
4294
|
-
|
|
4345
|
+
if (LokotroPayEnv.debugMode) {
|
|
4346
|
+
console.error('[Lokotro Pay] Form submission error:', err?.message || err);
|
|
4347
|
+
}
|
|
4295
4348
|
}
|
|
4296
4349
|
});
|
|
4297
4350
|
}
|
|
@@ -4301,9 +4354,12 @@ class LokotroPayCheckoutComponent {
|
|
|
4301
4354
|
this.paymentService.verifyOtp({
|
|
4302
4355
|
paymentId: this.state.transactionId,
|
|
4303
4356
|
otpCode: otp
|
|
4304
|
-
}).
|
|
4357
|
+
}).pipe(takeUntil(this.destroy$))
|
|
4358
|
+
.subscribe({
|
|
4305
4359
|
error: (err) => {
|
|
4306
|
-
|
|
4360
|
+
if (LokotroPayEnv.debugMode) {
|
|
4361
|
+
console.error('[Lokotro Pay] OTP verification error:', err?.message || err);
|
|
4362
|
+
}
|
|
4307
4363
|
}
|
|
4308
4364
|
});
|
|
4309
4365
|
}
|
|
@@ -4312,9 +4368,12 @@ class LokotroPayCheckoutComponent {
|
|
|
4312
4368
|
return;
|
|
4313
4369
|
this.paymentService.resendOtp({
|
|
4314
4370
|
paymentId: this.state.transactionId
|
|
4315
|
-
}).
|
|
4371
|
+
}).pipe(takeUntil(this.destroy$))
|
|
4372
|
+
.subscribe({
|
|
4316
4373
|
error: (err) => {
|
|
4317
|
-
|
|
4374
|
+
if (LokotroPayEnv.debugMode) {
|
|
4375
|
+
console.error('[Lokotro Pay] OTP resend error:', err?.message || err);
|
|
4376
|
+
}
|
|
4318
4377
|
}
|
|
4319
4378
|
});
|
|
4320
4379
|
}
|