@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, throwError, Subject, interval } from 'rxjs';
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: 'http://10.37.31.218:6495',
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(`[Lokotro HTTP #${this.instanceId}] WARNING: appKey is not set, app-key header NOT added`);
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
- console.log('[Lokotro Payment] Starting mobile money status polling for:', transactionId);
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).subscribe({
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
- console.log(`[Lokotro Payment] Mobile money poll #${this.mobileMoneyPollingAttempts}: status=${status}`);
3611
- if (status === 'completed' || status === 'success' || status === 'approved') {
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 (status === 'failed' || status === 'error' || status === 'cancelled' || status === 'declined') {
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
- console.error('[Lokotro Payment] Mobile money polling error:', err);
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.resetState();
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).subscribe({
4269
+ this.paymentService.createPayment(this.paymentBody)
4270
+ .pipe(takeUntil(this.destroy$))
4271
+ .subscribe({
4225
4272
  error: (err) => {
4226
- console.error('[Lokotro Pay] Payment initialization error:', err);
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).subscribe({
4341
+ this.paymentService.submitPaymentDetails(submitRequest)
4342
+ .pipe(takeUntil(this.destroy$))
4343
+ .subscribe({
4293
4344
  error: (err) => {
4294
- console.error('[Lokotro Pay] Form submission error:', err);
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
- }).subscribe({
4357
+ }).pipe(takeUntil(this.destroy$))
4358
+ .subscribe({
4305
4359
  error: (err) => {
4306
- console.error('[Lokotro Pay] OTP verification error:', err);
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
- }).subscribe({
4371
+ }).pipe(takeUntil(this.destroy$))
4372
+ .subscribe({
4316
4373
  error: (err) => {
4317
- console.error('[Lokotro Pay] OTP resend error:', err);
4374
+ if (LokotroPayEnv.debugMode) {
4375
+ console.error('[Lokotro Pay] OTP resend error:', err?.message || err);
4376
+ }
4318
4377
  }
4319
4378
  });
4320
4379
  }