@bloonio/lokotro-pay 1.5.1 → 1.6.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.
|
@@ -253,6 +253,7 @@ var LokotroPayScreenNavigation;
|
|
|
253
253
|
LokotroPayScreenNavigation["MobileMoneyFormScreen"] = "mobilemoneyFormScreen";
|
|
254
254
|
LokotroPayScreenNavigation["CardFormScreen"] = "cardFormScreen";
|
|
255
255
|
LokotroPayScreenNavigation["BankTransferFormScreen"] = "bankTransferFormScreen";
|
|
256
|
+
LokotroPayScreenNavigation["BankTransferOfferScreen"] = "bankTransferOfferScreen";
|
|
256
257
|
LokotroPayScreenNavigation["FlashFormScreen"] = "flashFormScreen";
|
|
257
258
|
LokotroPayScreenNavigation["PaymentConfirmationScreen"] = "paymentConfirmationScreen";
|
|
258
259
|
LokotroPayScreenNavigation["ProcessingScreen"] = "processingScreen";
|
|
@@ -275,6 +276,7 @@ const LokotroPayScreenNavigationInfo = {
|
|
|
275
276
|
[LokotroPayScreenNavigation.MobileMoneyFormScreen]: 'Mobile Money Form',
|
|
276
277
|
[LokotroPayScreenNavigation.CardFormScreen]: 'Card Form',
|
|
277
278
|
[LokotroPayScreenNavigation.BankTransferFormScreen]: 'Bank Transfer Form',
|
|
279
|
+
[LokotroPayScreenNavigation.BankTransferOfferScreen]: 'Bank Transfer Offer',
|
|
278
280
|
[LokotroPayScreenNavigation.FlashFormScreen]: 'Flash Form',
|
|
279
281
|
[LokotroPayScreenNavigation.PaymentConfirmationScreen]: 'Payment Confirmation',
|
|
280
282
|
[LokotroPayScreenNavigation.ProcessingScreen]: 'Processing',
|
|
@@ -629,6 +631,10 @@ const EN_TRANSLATIONS = {
|
|
|
629
631
|
accountLabel: 'Account Label',
|
|
630
632
|
bankTransferProofInstructions: 'A payment link will be sent to your email to upload the proof of transfer.',
|
|
631
633
|
confirmBankTransfer: 'Confirm Transfer',
|
|
634
|
+
bankTransferSwitchTitle: 'Payment method unavailable',
|
|
635
|
+
bankTransferSwitchMessage: "This amount isn't available for the selected payment method. Would you like to continue with a bank transfer instead?",
|
|
636
|
+
bankTransferSwitchConfirm: 'Continue with bank transfer',
|
|
637
|
+
bankTransferSwitchDecline: 'Choose another method',
|
|
632
638
|
// Auto-redirect countdown
|
|
633
639
|
redirectingIn: 'Redirecting in {seconds}s',
|
|
634
640
|
tapToCancel: 'Tap to cancel'
|
|
@@ -737,6 +743,10 @@ const FR_TRANSLATIONS = {
|
|
|
737
743
|
accountLabel: 'Libellé du compte',
|
|
738
744
|
bankTransferProofInstructions: 'Un lien de paiement sera envoyé à votre adresse e-mail pour télécharger la preuve de virement.',
|
|
739
745
|
confirmBankTransfer: 'Confirmer le virement',
|
|
746
|
+
bankTransferSwitchTitle: 'Moyen de paiement indisponible',
|
|
747
|
+
bankTransferSwitchMessage: "Ce montant n'est pas disponible pour le moyen de paiement choisi. Souhaitez-vous continuer avec un virement bancaire ?",
|
|
748
|
+
bankTransferSwitchConfirm: 'Continuer avec un virement',
|
|
749
|
+
bankTransferSwitchDecline: 'Choisir un autre moyen',
|
|
740
750
|
// Auto-redirect countdown
|
|
741
751
|
redirectingIn: 'Redirection dans {seconds}s',
|
|
742
752
|
tapToCancel: 'Appuyez pour annuler'
|
|
@@ -845,6 +855,10 @@ const ES_TRANSLATIONS = {
|
|
|
845
855
|
accountLabel: 'Etiqueta de la cuenta',
|
|
846
856
|
bankTransferProofInstructions: 'Se enviará un enlace de pago a su correo electrónico para cargar el comprobante de transferencia.',
|
|
847
857
|
confirmBankTransfer: 'Confirmar transferencia',
|
|
858
|
+
bankTransferSwitchTitle: 'Método de pago no disponible',
|
|
859
|
+
bankTransferSwitchMessage: 'Este importe no está disponible para el método de pago seleccionado. ¿Desea continuar con una transferencia bancaria?',
|
|
860
|
+
bankTransferSwitchConfirm: 'Continuar con transferencia',
|
|
861
|
+
bankTransferSwitchDecline: 'Elegir otro método',
|
|
848
862
|
// Auto-redirect countdown
|
|
849
863
|
redirectingIn: 'Redirigiendo en {seconds}s',
|
|
850
864
|
tapToCancel: 'Toque para cancelar'
|
|
@@ -2692,1568 +2706,2053 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
2692
2706
|
}] } });
|
|
2693
2707
|
|
|
2694
2708
|
/**
|
|
2695
|
-
* Lokotro Pay -
|
|
2709
|
+
* Lokotro Pay - Feature Flags
|
|
2710
|
+
*
|
|
2711
|
+
* Compile-time switches for capabilities that are partially wired into the SDK
|
|
2712
|
+
* but not yet ready to expose to end users. Flip a flag to `true` once the
|
|
2713
|
+
* feature is implemented end-to-end.
|
|
2696
2714
|
*/
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2715
|
+
const LOKOTRO_PAY_FEATURE_FLAGS = {
|
|
2716
|
+
/**
|
|
2717
|
+
* Native wallet payments — Google Pay and Apple Pay.
|
|
2718
|
+
*
|
|
2719
|
+
* The models and method-flag plumbing already understand these methods, but
|
|
2720
|
+
* the native tokenisation flows are not implemented end-to-end yet. While
|
|
2721
|
+
* this is `false` the SDK hides Google Pay / Apple Pay from the
|
|
2722
|
+
* payment-method selection UI so they can never be shown or selected. Set to
|
|
2723
|
+
* `true` once the native integrations are complete.
|
|
2724
|
+
*/
|
|
2725
|
+
nativeWalletsEnabled: false,
|
|
2726
|
+
};
|
|
2727
|
+
/**
|
|
2728
|
+
* Whether a payment method is a Google Pay / Apple Pay native wallet method.
|
|
2729
|
+
*
|
|
2730
|
+
* Matches the provider flag as well as the raw name / id / channel strings, so
|
|
2731
|
+
* detection holds even when the backend channel does not map onto the enum.
|
|
2732
|
+
*/
|
|
2733
|
+
function isNativeWalletMethod(method) {
|
|
2734
|
+
if (method.flag === 'google_pay' || method.flag === 'apple_pay') {
|
|
2735
|
+
return true;
|
|
2706
2736
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2737
|
+
const looksNative = (value) => {
|
|
2738
|
+
const key = (value ?? '').toLowerCase().replace(/[^a-z]/g, '');
|
|
2739
|
+
return key.includes('googlepay') || key.includes('applepay');
|
|
2740
|
+
};
|
|
2741
|
+
return (looksNative(method.name) ||
|
|
2742
|
+
looksNative(method.displayName) ||
|
|
2743
|
+
looksNative(method.id) ||
|
|
2744
|
+
looksNative(method.channel));
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Returns the payment methods that should be visible to the user, dropping any
|
|
2748
|
+
* hidden behind a disabled feature flag (currently Google / Apple Pay). The
|
|
2749
|
+
* list is returned unchanged once the relevant flag is enabled.
|
|
2750
|
+
*/
|
|
2751
|
+
function visiblePaymentMethods(methods) {
|
|
2752
|
+
if (LOKOTRO_PAY_FEATURE_FLAGS.nativeWalletsEnabled) {
|
|
2753
|
+
return methods;
|
|
2710
2754
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2755
|
+
return methods.filter((method) => !isNativeWalletMethod(method));
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
/**
|
|
2759
|
+
* Lokotro Pay - Payment Service
|
|
2760
|
+
* Main payment service that follows the Lokotro Gateway API flow
|
|
2761
|
+
*/
|
|
2762
|
+
/**
|
|
2763
|
+
* Initial payment state
|
|
2764
|
+
*/
|
|
2765
|
+
const initialState = {
|
|
2766
|
+
isLoading: false,
|
|
2767
|
+
currentScreen: LokotroPayScreenNavigation.LoadingScreen
|
|
2768
|
+
};
|
|
2769
|
+
class LokotroPaymentService {
|
|
2770
|
+
static { this.MOBILE_MONEY_POLL_INTERVAL = 5000; }
|
|
2771
|
+
static { this.MOBILE_MONEY_MAX_ATTEMPTS = 60; }
|
|
2772
|
+
constructor(httpClient) {
|
|
2773
|
+
this.httpClient = httpClient;
|
|
2774
|
+
this.stateSubject = new BehaviorSubject(initialState);
|
|
2775
|
+
this.state$ = this.stateSubject.asObservable();
|
|
2776
|
+
this.mobileMoneyPollingAttempts = 0;
|
|
2715
2777
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2778
|
+
/**
|
|
2779
|
+
* Get current state
|
|
2780
|
+
*/
|
|
2781
|
+
get currentState() {
|
|
2782
|
+
return this.stateSubject.value;
|
|
2718
2783
|
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2784
|
+
/**
|
|
2785
|
+
* Update state
|
|
2786
|
+
*/
|
|
2787
|
+
updateState(partialState) {
|
|
2788
|
+
this.stateSubject.next({
|
|
2789
|
+
...this.currentState,
|
|
2790
|
+
...partialState
|
|
2791
|
+
});
|
|
2721
2792
|
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
}, 1000);
|
|
2793
|
+
/**
|
|
2794
|
+
* Reset state to initial
|
|
2795
|
+
*/
|
|
2796
|
+
resetState() {
|
|
2797
|
+
this.stopMobileMoneyPolling();
|
|
2798
|
+
this.currentPaymentBody = undefined;
|
|
2799
|
+
this.stateSubject.next(initialState);
|
|
2730
2800
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
input.value = value[0];
|
|
2737
|
-
// Focus next input
|
|
2738
|
-
if (index < this.otpLength - 1) {
|
|
2739
|
-
const inputs = document.querySelectorAll('.lokotro-otp-input');
|
|
2740
|
-
inputs[index + 1]?.focus();
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
else {
|
|
2744
|
-
this.otpDigits[index] = '';
|
|
2745
|
-
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Set app-key for authentication
|
|
2803
|
+
*/
|
|
2804
|
+
setAppKey(appKey) {
|
|
2805
|
+
this.httpClient.setAppKey(appKey);
|
|
2746
2806
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2807
|
+
/**
|
|
2808
|
+
* Set accept language
|
|
2809
|
+
*/
|
|
2810
|
+
setAcceptLanguage(language) {
|
|
2811
|
+
this.httpClient.setAcceptLanguage(language);
|
|
2752
2812
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2813
|
+
/**
|
|
2814
|
+
* Step 1: Create payment transaction
|
|
2815
|
+
* POST /payments/collect
|
|
2816
|
+
*/
|
|
2817
|
+
createPayment(paymentBody) {
|
|
2818
|
+
this.currentPaymentBody = paymentBody;
|
|
2819
|
+
this.updateState({ isLoading: true });
|
|
2820
|
+
const requestData = this.convertPaymentBodyToRequest(paymentBody);
|
|
2821
|
+
// PR-1.2 / CRIT-1: previous `console.log(requestData)` shipped PAN, CVV,
|
|
2822
|
+
// wallet PIN, flash PIN, and full PII to the browser console on every
|
|
2823
|
+
// checkout. Removed.
|
|
2824
|
+
return this.httpClient.post(LokotroPayEnv.endpoints.collect, requestData).pipe(switchMap(response => {
|
|
2825
|
+
// PR-1.2: response contains transaction IDs and (sometimes) redirect
|
|
2826
|
+
// URLs that are sensitive. Don't log it.
|
|
2827
|
+
if (!response.isSuccess || !response.data) {
|
|
2828
|
+
this.updateState({
|
|
2829
|
+
isLoading: false,
|
|
2830
|
+
error: {
|
|
2831
|
+
message: response.message || 'Failed to create payment',
|
|
2832
|
+
title: 'Payment Creation Failed',
|
|
2833
|
+
errorCode: response.apiResponseCode,
|
|
2834
|
+
timestamp: new Date()
|
|
2835
|
+
},
|
|
2836
|
+
currentScreen: LokotroPayScreenNavigation.ErrorScreen
|
|
2837
|
+
});
|
|
2838
|
+
return of(response);
|
|
2759
2839
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2840
|
+
return this.resolveCollectResponse(this.unwrapPayload(response.data), paymentBody).pipe(map(() => response));
|
|
2841
|
+
}), catchError(error => {
|
|
2842
|
+
this.updateState({
|
|
2843
|
+
isLoading: false,
|
|
2844
|
+
error: {
|
|
2845
|
+
message: error.message || 'Failed to create payment',
|
|
2846
|
+
title: 'Error',
|
|
2847
|
+
timestamp: new Date()
|
|
2848
|
+
},
|
|
2849
|
+
currentScreen: LokotroPayScreenNavigation.ErrorScreen
|
|
2763
2850
|
});
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
inputs[focusIndex]?.focus();
|
|
2767
|
-
}
|
|
2768
|
-
}
|
|
2769
|
-
onVerify() {
|
|
2770
|
-
if (this.isOtpComplete) {
|
|
2771
|
-
this.otpVerified.emit(this.otpValue);
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
onResend() {
|
|
2775
|
-
this.otpDigits = new Array(this.otpLength).fill('');
|
|
2776
|
-
this.startResendTimer();
|
|
2777
|
-
this.resendOtp.emit();
|
|
2851
|
+
return throwError(() => error);
|
|
2852
|
+
}));
|
|
2778
2853
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2854
|
+
/**
|
|
2855
|
+
* Step 2: Get transaction details
|
|
2856
|
+
* GET /payments/transaction/{transaction_id}
|
|
2857
|
+
*/
|
|
2858
|
+
getTransactionDetails(transactionId) {
|
|
2859
|
+
return this.httpClient.get(`${LokotroPayEnv.endpoints.transaction}/${transactionId}`);
|
|
2781
2860
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
[disabled]="!isOtpComplete"
|
|
2835
|
-
(click)="onVerify()">
|
|
2836
|
-
{{ localization.translate('verifyOtp') }}
|
|
2837
|
-
</button>
|
|
2838
|
-
</div>
|
|
2839
|
-
</div>
|
|
2840
|
-
`, isInline: true, styles: [".lokotro-otp-verification{display:flex;flex-direction:column;align-items:center;text-align:center;padding:24px 0}.lokotro-otp-icon{width:80px;height:80px;display:flex;align-items:center;justify-content:center;background:var(--lokotro-card, #3A4840);border-radius:50%;margin-bottom:24px;color:var(--lokotro-accent, #3BFBDA)}.lokotro-otp-title{font-size:24px;font-weight:600;margin:0 0 8px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-otp-subtitle{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0 0 32px}.lokotro-otp-inputs{display:flex;gap:12px;margin-bottom:24px}.lokotro-otp-input{width:50px;height:56px;text-align:center;font-size:24px;font-weight:600;background:var(--lokotro-card, #3A4840);border:2px solid var(--lokotro-border, #3A473F);border-radius:12px;color:var(--lokotro-text-primary, #F2F0D5);transition:border-color .2s}.lokotro-otp-input:focus{outline:none;border-color:var(--lokotro-accent, #3BFBDA)}.lokotro-otp-timer{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin-bottom:24px}.lokotro-resend-btn{background:none;border:none;color:var(--lokotro-accent, #3BFBDA);font-size:14px;font-weight:600;cursor:pointer;margin-bottom:24px;text-decoration:underline}.lokotro-resend-btn:hover{opacity:.8}.lokotro-form-actions{display:flex;gap:12px;width:100%}.lokotro-btn-primary,.lokotro-btn-secondary{flex:1;padding:14px 24px;border-radius:12px;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s ease}.lokotro-btn-primary{background:linear-gradient(135deg,var(--lokotro-primary, #5A5E39),var(--lokotro-secondary, #6E7346));color:var(--lokotro-text-primary, #F2F0D5);border:none}.lokotro-btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 16px #0003}.lokotro-btn-primary:disabled{opacity:.5;cursor:not-allowed}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #F2F0D5);border:2px solid var(--lokotro-border, #3A473F)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #D5D3B8)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }] }); }
|
|
2841
|
-
}
|
|
2842
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroOtpVerificationComponent, decorators: [{
|
|
2843
|
-
type: Component,
|
|
2844
|
-
args: [{ selector: 'lokotro-otp-verification', standalone: true, imports: [FormsModule], template: `
|
|
2845
|
-
<div class="lokotro-otp-verification">
|
|
2846
|
-
<div class="lokotro-otp-icon">
|
|
2847
|
-
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
2848
|
-
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
2849
|
-
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
2850
|
-
</svg>
|
|
2851
|
-
</div>
|
|
2852
|
-
|
|
2853
|
-
<h3 class="lokotro-otp-title">{{ localization.translate('enterOtp') }}</h3>
|
|
2854
|
-
@if (otpDestination) {
|
|
2855
|
-
<p class="lokotro-otp-subtitle">
|
|
2856
|
-
{{ localization.translate('otpSentTo') }} {{ otpDestination }}
|
|
2857
|
-
</p>
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
<div class="lokotro-otp-inputs">
|
|
2861
|
-
@for (digit of otpDigits; track digit; let i = $index) {
|
|
2862
|
-
<input
|
|
2863
|
-
type="text"
|
|
2864
|
-
class="lokotro-otp-input"
|
|
2865
|
-
maxlength="1"
|
|
2866
|
-
[value]="otpDigits[i]"
|
|
2867
|
-
(input)="onOtpInput($event, i)"
|
|
2868
|
-
(keydown)="onOtpKeydown($event, i)"
|
|
2869
|
-
(paste)="onOtpPaste($event)"
|
|
2870
|
-
#otpInput>
|
|
2871
|
-
}
|
|
2872
|
-
</div>
|
|
2873
|
-
|
|
2874
|
-
@if (resendTimer > 0) {
|
|
2875
|
-
<div class="lokotro-otp-timer">
|
|
2876
|
-
Resend OTP in {{ resendTimer }}s
|
|
2877
|
-
</div>
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
@if (resendTimer === 0) {
|
|
2881
|
-
<button
|
|
2882
|
-
class="lokotro-resend-btn"
|
|
2883
|
-
(click)="onResend()">
|
|
2884
|
-
{{ localization.translate('resendOtp') }}
|
|
2885
|
-
</button>
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
<div class="lokotro-form-actions">
|
|
2889
|
-
<button type="button" class="lokotro-btn-secondary" (click)="onCancelClick()">
|
|
2890
|
-
{{ localization.translate('cancel') }}
|
|
2891
|
-
</button>
|
|
2892
|
-
<button
|
|
2893
|
-
type="button"
|
|
2894
|
-
class="lokotro-btn-primary"
|
|
2895
|
-
[disabled]="!isOtpComplete"
|
|
2896
|
-
(click)="onVerify()">
|
|
2897
|
-
{{ localization.translate('verifyOtp') }}
|
|
2898
|
-
</button>
|
|
2899
|
-
</div>
|
|
2900
|
-
</div>
|
|
2901
|
-
`, styles: [".lokotro-otp-verification{display:flex;flex-direction:column;align-items:center;text-align:center;padding:24px 0}.lokotro-otp-icon{width:80px;height:80px;display:flex;align-items:center;justify-content:center;background:var(--lokotro-card, #3A4840);border-radius:50%;margin-bottom:24px;color:var(--lokotro-accent, #3BFBDA)}.lokotro-otp-title{font-size:24px;font-weight:600;margin:0 0 8px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-otp-subtitle{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0 0 32px}.lokotro-otp-inputs{display:flex;gap:12px;margin-bottom:24px}.lokotro-otp-input{width:50px;height:56px;text-align:center;font-size:24px;font-weight:600;background:var(--lokotro-card, #3A4840);border:2px solid var(--lokotro-border, #3A473F);border-radius:12px;color:var(--lokotro-text-primary, #F2F0D5);transition:border-color .2s}.lokotro-otp-input:focus{outline:none;border-color:var(--lokotro-accent, #3BFBDA)}.lokotro-otp-timer{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin-bottom:24px}.lokotro-resend-btn{background:none;border:none;color:var(--lokotro-accent, #3BFBDA);font-size:14px;font-weight:600;cursor:pointer;margin-bottom:24px;text-decoration:underline}.lokotro-resend-btn:hover{opacity:.8}.lokotro-form-actions{display:flex;gap:12px;width:100%}.lokotro-btn-primary,.lokotro-btn-secondary{flex:1;padding:14px 24px;border-radius:12px;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s ease}.lokotro-btn-primary{background:linear-gradient(135deg,var(--lokotro-primary, #5A5E39),var(--lokotro-secondary, #6E7346));color:var(--lokotro-text-primary, #F2F0D5);border:none}.lokotro-btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 16px #0003}.lokotro-btn-primary:disabled{opacity:.5;cursor:not-allowed}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #F2F0D5);border:2px solid var(--lokotro-border, #3A473F)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #D5D3B8)}\n"] }]
|
|
2902
|
-
}], ctorParameters: () => [{ type: LokotroLocalizationService }], propDecorators: { transactionId: [{
|
|
2903
|
-
type: Input
|
|
2904
|
-
}], otpDestination: [{
|
|
2905
|
-
type: Input
|
|
2906
|
-
}], otpLength: [{
|
|
2907
|
-
type: Input
|
|
2908
|
-
}], otpVerified: [{
|
|
2909
|
-
type: Output
|
|
2910
|
-
}], resendOtp: [{
|
|
2911
|
-
type: Output
|
|
2912
|
-
}], cancel: [{
|
|
2913
|
-
type: Output
|
|
2914
|
-
}] } });
|
|
2915
|
-
|
|
2916
|
-
/**
|
|
2917
|
-
* Lokotro Pay - Processing Component
|
|
2918
|
-
*/
|
|
2919
|
-
class LokotroProcessingComponent {
|
|
2920
|
-
constructor(localization) {
|
|
2921
|
-
this.localization = localization;
|
|
2922
|
-
this.type = 'default';
|
|
2923
|
-
this.currentStep = 1;
|
|
2924
|
-
// Simulate step progression for mobile money
|
|
2925
|
-
if (this.type === 'mobileMoney') {
|
|
2926
|
-
setTimeout(() => this.currentStep = 2, 3000);
|
|
2927
|
-
setTimeout(() => this.currentStep = 3, 6000);
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroProcessingComponent, deps: [{ token: LokotroLocalizationService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2931
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: LokotroProcessingComponent, isStandalone: true, selector: "lokotro-processing", inputs: { type: "type", message: "message" }, ngImport: i0, template: `
|
|
2932
|
-
<div class="lokotro-processing">
|
|
2933
|
-
<div class="lokotro-processing-spinner">
|
|
2934
|
-
<div class="lokotro-spinner-ring"></div>
|
|
2935
|
-
<div class="lokotro-spinner-ring"></div>
|
|
2936
|
-
<div class="lokotro-spinner-ring"></div>
|
|
2937
|
-
</div>
|
|
2938
|
-
|
|
2939
|
-
<h3 class="lokotro-processing-title">{{ message || localization.translate('processing') }}</h3>
|
|
2940
|
-
|
|
2941
|
-
@if (type === 'mobileMoney') {
|
|
2942
|
-
<p class="lokotro-processing-subtitle">
|
|
2943
|
-
Please confirm the payment on your mobile device
|
|
2944
|
-
</p>
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
|
-
@if (type === 'mobileMoney') {
|
|
2948
|
-
<div class="lokotro-processing-steps">
|
|
2949
|
-
<div class="lokotro-step" [class.active]="currentStep >= 1">
|
|
2950
|
-
<div class="lokotro-step-icon">📱</div>
|
|
2951
|
-
<span>Check your phone</span>
|
|
2952
|
-
</div>
|
|
2953
|
-
<div class="lokotro-step" [class.active]="currentStep >= 2">
|
|
2954
|
-
<div class="lokotro-step-icon">🔐</div>
|
|
2955
|
-
<span>Enter your PIN</span>
|
|
2956
|
-
</div>
|
|
2957
|
-
<div class="lokotro-step" [class.active]="currentStep >= 3">
|
|
2958
|
-
<div class="lokotro-step-icon">✅</div>
|
|
2959
|
-
<span>Confirm payment</span>
|
|
2960
|
-
</div>
|
|
2961
|
-
</div>
|
|
2962
|
-
}
|
|
2963
|
-
</div>
|
|
2964
|
-
`, isInline: true, styles: [".lokotro-processing{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:48px 24px;min-height:300px}.lokotro-processing-spinner{position:relative;width:80px;height:80px;margin-bottom:32px}.lokotro-spinner-ring{position:absolute;width:100%;height:100%;border:3px solid transparent;border-radius:50%;animation:lokotro-spin 1.5s linear infinite}.lokotro-spinner-ring:nth-child(1){border-top-color:var(--lokotro-accent, #3BFBDA);animation-delay:0s}.lokotro-spinner-ring:nth-child(2){border-right-color:var(--lokotro-primary, #5A5E39);animation-delay:.15s;width:70%;height:70%;top:15%;left:15%}.lokotro-spinner-ring:nth-child(3){border-bottom-color:var(--lokotro-secondary, #6E7346);animation-delay:.3s;width:50%;height:50%;top:25%;left:25%}@keyframes lokotro-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.lokotro-processing-title{font-size:20px;font-weight:600;margin:0 0 8px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-processing-subtitle{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0 0 32px}.lokotro-processing-steps{display:flex;flex-direction:column;gap:16px;width:100%;max-width:280px}.lokotro-step{display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--lokotro-card, #3A4840);border-radius:12px;opacity:.5;transition:opacity .3s}.lokotro-step.active{opacity:1}.lokotro-step-icon{font-size:20px}.lokotro-step span{font-size:14px;color:var(--lokotro-text-primary, #F2F0D5)}\n"] }); }
|
|
2965
|
-
}
|
|
2966
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroProcessingComponent, decorators: [{
|
|
2967
|
-
type: Component,
|
|
2968
|
-
args: [{ selector: 'lokotro-processing', standalone: true, imports: [], template: `
|
|
2969
|
-
<div class="lokotro-processing">
|
|
2970
|
-
<div class="lokotro-processing-spinner">
|
|
2971
|
-
<div class="lokotro-spinner-ring"></div>
|
|
2972
|
-
<div class="lokotro-spinner-ring"></div>
|
|
2973
|
-
<div class="lokotro-spinner-ring"></div>
|
|
2974
|
-
</div>
|
|
2975
|
-
|
|
2976
|
-
<h3 class="lokotro-processing-title">{{ message || localization.translate('processing') }}</h3>
|
|
2977
|
-
|
|
2978
|
-
@if (type === 'mobileMoney') {
|
|
2979
|
-
<p class="lokotro-processing-subtitle">
|
|
2980
|
-
Please confirm the payment on your mobile device
|
|
2981
|
-
</p>
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
|
-
@if (type === 'mobileMoney') {
|
|
2985
|
-
<div class="lokotro-processing-steps">
|
|
2986
|
-
<div class="lokotro-step" [class.active]="currentStep >= 1">
|
|
2987
|
-
<div class="lokotro-step-icon">📱</div>
|
|
2988
|
-
<span>Check your phone</span>
|
|
2989
|
-
</div>
|
|
2990
|
-
<div class="lokotro-step" [class.active]="currentStep >= 2">
|
|
2991
|
-
<div class="lokotro-step-icon">🔐</div>
|
|
2992
|
-
<span>Enter your PIN</span>
|
|
2993
|
-
</div>
|
|
2994
|
-
<div class="lokotro-step" [class.active]="currentStep >= 3">
|
|
2995
|
-
<div class="lokotro-step-icon">✅</div>
|
|
2996
|
-
<span>Confirm payment</span>
|
|
2997
|
-
</div>
|
|
2998
|
-
</div>
|
|
2999
|
-
}
|
|
3000
|
-
</div>
|
|
3001
|
-
`, styles: [".lokotro-processing{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:48px 24px;min-height:300px}.lokotro-processing-spinner{position:relative;width:80px;height:80px;margin-bottom:32px}.lokotro-spinner-ring{position:absolute;width:100%;height:100%;border:3px solid transparent;border-radius:50%;animation:lokotro-spin 1.5s linear infinite}.lokotro-spinner-ring:nth-child(1){border-top-color:var(--lokotro-accent, #3BFBDA);animation-delay:0s}.lokotro-spinner-ring:nth-child(2){border-right-color:var(--lokotro-primary, #5A5E39);animation-delay:.15s;width:70%;height:70%;top:15%;left:15%}.lokotro-spinner-ring:nth-child(3){border-bottom-color:var(--lokotro-secondary, #6E7346);animation-delay:.3s;width:50%;height:50%;top:25%;left:25%}@keyframes lokotro-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.lokotro-processing-title{font-size:20px;font-weight:600;margin:0 0 8px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-processing-subtitle{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0 0 32px}.lokotro-processing-steps{display:flex;flex-direction:column;gap:16px;width:100%;max-width:280px}.lokotro-step{display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--lokotro-card, #3A4840);border-radius:12px;opacity:.5;transition:opacity .3s}.lokotro-step.active{opacity:1}.lokotro-step-icon{font-size:20px}.lokotro-step span{font-size:14px;color:var(--lokotro-text-primary, #F2F0D5)}\n"] }]
|
|
3002
|
-
}], ctorParameters: () => [{ type: LokotroLocalizationService }], propDecorators: { type: [{
|
|
3003
|
-
type: Input
|
|
3004
|
-
}], message: [{
|
|
3005
|
-
type: Input
|
|
3006
|
-
}] } });
|
|
3007
|
-
|
|
3008
|
-
/**
|
|
3009
|
-
* Lokotro Pay - Result Component
|
|
3010
|
-
* Success, Error, Warning, Info screens
|
|
3011
|
-
*
|
|
3012
|
-
* Designed for financial UX: clean white surface, prominent amount focal
|
|
3013
|
-
* on success, filled status badge, restrained transaction-detail panel.
|
|
3014
|
-
*/
|
|
3015
|
-
class LokotroResultComponent {
|
|
3016
|
-
constructor(localization) {
|
|
3017
|
-
this.localization = localization;
|
|
3018
|
-
this.type = 'success';
|
|
3019
|
-
this.title = '';
|
|
3020
|
-
/** Auto-redirect duration in seconds. If > 0, will auto-call onAutoRedirect after countdown. */
|
|
3021
|
-
this.autoRedirectSeconds = 0;
|
|
3022
|
-
this.primaryAction = new EventEmitter();
|
|
3023
|
-
this.secondaryAction = new EventEmitter();
|
|
3024
|
-
/** Emitted when auto-redirect triggers after countdown completes. */
|
|
3025
|
-
this.autoRedirect = new EventEmitter();
|
|
3026
|
-
// Countdown state
|
|
3027
|
-
this.countdownSeconds = 0;
|
|
3028
|
-
this.isCountdownCancelled = false;
|
|
3029
|
-
}
|
|
3030
|
-
ngOnInit() {
|
|
3031
|
-
this.startAutoRedirectCountdown();
|
|
3032
|
-
}
|
|
3033
|
-
ngOnDestroy() {
|
|
3034
|
-
this.clearCountdown();
|
|
3035
|
-
}
|
|
3036
|
-
startAutoRedirectCountdown() {
|
|
3037
|
-
if (this.autoRedirectSeconds <= 0)
|
|
3038
|
-
return;
|
|
3039
|
-
this.countdownSeconds = this.autoRedirectSeconds;
|
|
3040
|
-
this.countdownInterval = setInterval(() => {
|
|
3041
|
-
if (this.isCountdownCancelled) {
|
|
3042
|
-
this.clearCountdown();
|
|
3043
|
-
return;
|
|
3044
|
-
}
|
|
3045
|
-
this.countdownSeconds--;
|
|
3046
|
-
if (this.countdownSeconds <= 0) {
|
|
3047
|
-
this.clearCountdown();
|
|
3048
|
-
this.triggerAutoRedirect();
|
|
2861
|
+
/**
|
|
2862
|
+
* Step 3: Submit payment details
|
|
2863
|
+
* POST /payments/submit
|
|
2864
|
+
*/
|
|
2865
|
+
submitPaymentDetails(request) {
|
|
2866
|
+
this.updateState({
|
|
2867
|
+
isLoading: true,
|
|
2868
|
+
currentScreen: LokotroPayScreenNavigation.ProcessingScreen
|
|
2869
|
+
});
|
|
2870
|
+
const requestData = this.convertSubmitRequestToData(request);
|
|
2871
|
+
return this.httpClient.post(LokotroPayEnv.endpoints.submit, requestData).pipe(tap(response => {
|
|
2872
|
+
if (response.isSuccess && response.data) {
|
|
2873
|
+
const responseData = this.unwrapPayload(response.data);
|
|
2874
|
+
const submitResponse = this.parseSubmitResponse(responseData);
|
|
2875
|
+
if (submitResponse.requiresOtp) {
|
|
2876
|
+
this.updateState({
|
|
2877
|
+
isLoading: false,
|
|
2878
|
+
transactionId: submitResponse.transactionId || this.resolvePaymentId(request),
|
|
2879
|
+
currentScreen: LokotroPayScreenNavigation.EWalletOtpScreen
|
|
2880
|
+
});
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
if (submitResponse.redirectUrl) {
|
|
2884
|
+
this.updateState({
|
|
2885
|
+
isLoading: false,
|
|
2886
|
+
transactionId: submitResponse.transactionId || this.resolvePaymentId(request)
|
|
2887
|
+
});
|
|
2888
|
+
// PR-1.2 / Angular HIGH-1: validate redirect URL against allow-list
|
|
2889
|
+
// before handing the tab over. Server-side allow-list (PR-2.0) is
|
|
2890
|
+
// the real defense; this is defense-in-depth on the client.
|
|
2891
|
+
this.redirectIfAllowed(submitResponse.redirectUrl, 'submit-response');
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
if (submitResponse.status === LokotroPaymentStatus.PendingBankProofUpload) {
|
|
2895
|
+
this.handlePaymentSuccess(responseData, LokotroPaymentStatus.PendingBankProofUpload, 'Upload Proof Required');
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
if (LokotroPaymentStatusInfo.isSuccess(submitResponse.status)) {
|
|
2899
|
+
this.handlePaymentSuccess(responseData);
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
if (LokotroPaymentStatusInfo.isPending(submitResponse.status)) {
|
|
2903
|
+
const txId = submitResponse.transactionId || this.resolvePaymentId(request);
|
|
2904
|
+
this.updateState({
|
|
2905
|
+
isLoading: false,
|
|
2906
|
+
transactionId: txId,
|
|
2907
|
+
currentScreen: LokotroPayScreenNavigation.MobileMoneyProcessingScreen
|
|
2908
|
+
});
|
|
2909
|
+
this.startMobileMoneyPolling(txId);
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
this.handlePaymentFailure(this.getString(responseData['message']) || response.message, response.apiResponseCode);
|
|
3049
2913
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
this.
|
|
3055
|
-
|
|
3056
|
-
}
|
|
3057
|
-
}
|
|
3058
|
-
clearCountdown() {
|
|
3059
|
-
if (this.countdownInterval) {
|
|
3060
|
-
clearInterval(this.countdownInterval);
|
|
3061
|
-
this.countdownInterval = undefined;
|
|
3062
|
-
}
|
|
3063
|
-
}
|
|
3064
|
-
triggerAutoRedirect() {
|
|
3065
|
-
if (this.isCountdownCancelled)
|
|
3066
|
-
return;
|
|
3067
|
-
this.autoRedirect.emit();
|
|
3068
|
-
}
|
|
3069
|
-
getCountdownProgress() {
|
|
3070
|
-
if (this.autoRedirectSeconds <= 0)
|
|
3071
|
-
return 0;
|
|
3072
|
-
return (this.countdownSeconds / this.autoRedirectSeconds) * 100;
|
|
2914
|
+
else {
|
|
2915
|
+
this.handlePaymentFailure(response.message, response.apiResponseCode);
|
|
2916
|
+
}
|
|
2917
|
+
}), catchError(error => {
|
|
2918
|
+
this.handlePaymentFailure(error.message);
|
|
2919
|
+
return throwError(() => error);
|
|
2920
|
+
}));
|
|
3073
2921
|
}
|
|
3074
|
-
|
|
3075
|
-
|
|
2922
|
+
/**
|
|
2923
|
+
* Step 4: Verify OTP (for e-wallet and flash payments)
|
|
2924
|
+
* POST /payments/verify-otp
|
|
2925
|
+
*/
|
|
2926
|
+
verifyOtp(request) {
|
|
2927
|
+
this.updateState({ isLoading: true });
|
|
2928
|
+
return this.httpClient.post(LokotroPayEnv.endpoints.verifyOtp, {
|
|
2929
|
+
payment_id: request.paymentId || request.transactionId || this.currentState.transactionId,
|
|
2930
|
+
otp_code: request.otpCode || request.otp
|
|
2931
|
+
}).pipe(tap(response => {
|
|
2932
|
+
if (response.isSuccess && response.data) {
|
|
2933
|
+
const responseData = this.unwrapPayload(response.data);
|
|
2934
|
+
const status = LokotroPaymentStatusInfo.fromString(this.getString(responseData['status']));
|
|
2935
|
+
if (LokotroPaymentStatusInfo.isSuccess(status)) {
|
|
2936
|
+
this.handlePaymentSuccess(responseData);
|
|
2937
|
+
}
|
|
2938
|
+
else {
|
|
2939
|
+
this.handlePaymentFailure(this.getString(responseData['message']) || response.message, response.apiResponseCode);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
else {
|
|
2943
|
+
this.updateState({
|
|
2944
|
+
isLoading: false,
|
|
2945
|
+
error: {
|
|
2946
|
+
message: response.message || 'OTP verification failed',
|
|
2947
|
+
title: 'Verification Failed',
|
|
2948
|
+
errorCode: response.apiResponseCode,
|
|
2949
|
+
timestamp: new Date()
|
|
2950
|
+
},
|
|
2951
|
+
currentScreen: LokotroPayScreenNavigation.ErrorScreen
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
}), catchError(error => {
|
|
2955
|
+
this.updateState({
|
|
2956
|
+
isLoading: false,
|
|
2957
|
+
error: {
|
|
2958
|
+
message: error.message || 'OTP verification failed',
|
|
2959
|
+
title: 'Error',
|
|
2960
|
+
timestamp: new Date()
|
|
2961
|
+
},
|
|
2962
|
+
currentScreen: LokotroPayScreenNavigation.ErrorScreen
|
|
2963
|
+
});
|
|
2964
|
+
return throwError(() => error);
|
|
2965
|
+
}));
|
|
3076
2966
|
}
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
2967
|
+
/**
|
|
2968
|
+
* Step 5: Resend OTP (if needed)
|
|
2969
|
+
* POST /payments/resend-otp
|
|
2970
|
+
*/
|
|
2971
|
+
resendOtp(request) {
|
|
2972
|
+
return this.httpClient.post(LokotroPayEnv.endpoints.resendOtp, {
|
|
2973
|
+
payment_id: request.paymentId || request.transactionId || this.currentState.transactionId
|
|
2974
|
+
});
|
|
3083
2975
|
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
2976
|
+
/**
|
|
2977
|
+
* Select payment method
|
|
2978
|
+
*/
|
|
2979
|
+
selectPaymentMethod(method) {
|
|
2980
|
+
this.updateState({
|
|
2981
|
+
selectedPaymentMethod: method,
|
|
2982
|
+
currentScreen: this.getFormScreenForChannel(method.channel)
|
|
2983
|
+
});
|
|
3087
2984
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
2985
|
+
/**
|
|
2986
|
+
* Navigate to screen
|
|
2987
|
+
*/
|
|
2988
|
+
navigateToScreen(screen) {
|
|
2989
|
+
this.updateState({ currentScreen: screen });
|
|
3091
2990
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
<!-- Countdown Indicator -->
|
|
3140
|
-
@if (autoRedirectSeconds > 0 && countdownSeconds > 0 && !isCountdownCancelled) {
|
|
3141
|
-
<button type="button" class="lokotro-countdown-indicator" (click)="cancelCountdown()">
|
|
3142
|
-
<svg viewBox="0 0 36 36" class="lokotro-countdown-circle">
|
|
3143
|
-
<path class="lokotro-countdown-bg"
|
|
3144
|
-
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
|
3145
|
-
<path class="lokotro-countdown-fill" [class]="'lokotro-countdown-' + type"
|
|
3146
|
-
[attr.stroke-dasharray]="getCountdownProgress() + ', 100'"
|
|
3147
|
-
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
|
3148
|
-
</svg>
|
|
3149
|
-
<span class="lokotro-countdown-text">
|
|
3150
|
-
{{ localization.translate('redirectingIn', { seconds: countdownSeconds }) }}
|
|
3151
|
-
</span>
|
|
3152
|
-
<svg class="lokotro-countdown-cancel" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3153
|
-
<path d="M18 6L6 18M6 6l12 12"/>
|
|
3154
|
-
</svg>
|
|
3155
|
-
</button>
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
|
-
<!-- Transaction Details -->
|
|
3159
|
-
@if (type === 'success' && transactionId) {
|
|
3160
|
-
<div class="lokotro-result-details">
|
|
3161
|
-
<div class="lokotro-detail-row">
|
|
3162
|
-
<span class="lokotro-detail-label">Transaction ID</span>
|
|
3163
|
-
<span class="lokotro-detail-value lokotro-mono">{{ transactionId }}</span>
|
|
3164
|
-
</div>
|
|
3165
|
-
</div>
|
|
3166
|
-
}
|
|
3167
|
-
</div>
|
|
3168
|
-
|
|
3169
|
-
<!-- Actions -->
|
|
3170
|
-
@if (primaryActionLabel || secondaryActionLabel) {
|
|
3171
|
-
<div class="lokotro-result-actions">
|
|
3172
|
-
@if (primaryActionLabel) {
|
|
3173
|
-
<button type="button" class="lokotro-btn-primary" (click)="onPrimaryClick()">
|
|
3174
|
-
{{ primaryActionLabel }}
|
|
3175
|
-
</button>
|
|
3176
|
-
}
|
|
3177
|
-
@if (secondaryActionLabel) {
|
|
3178
|
-
<button type="button" class="lokotro-btn-secondary" (click)="onSecondaryClick()">
|
|
3179
|
-
{{ secondaryActionLabel }}
|
|
3180
|
-
</button>
|
|
3181
|
-
}
|
|
3182
|
-
</div>
|
|
3183
|
-
}
|
|
3184
|
-
</div>
|
|
3185
|
-
`, isInline: true, styles: [".lokotro-result{display:flex;flex-direction:column;align-items:stretch;max-width:420px;margin:0 auto;padding:24px 16px;gap:20px}.lokotro-result-card{display:flex;flex-direction:column;align-items:center;text-align:center;padding:32px 24px;background:var(--lokotro-card, #FFFFFF);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:20px;box-shadow:0 1px 3px var(--lokotro-shadow-light, rgba(15, 23, 42, .04)),0 8px 24px var(--lokotro-shadow-light, rgba(15, 23, 42, .04))}.lokotro-status-badge{width:72px;height:72px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:24px;animation:lokotro-pop .5s cubic-bezier(.34,1.56,.64,1)}.lokotro-status-badge svg{width:36px;height:36px;color:#fff}.lokotro-status-success{background:var(--lokotro-success, #16A34A);box-shadow:0 0 0 8px #16a34a1f,0 8px 20px #16a34a40}.lokotro-status-error{background:var(--lokotro-error, #DC2626);box-shadow:0 0 0 8px #dc26261f,0 8px 20px #dc262640}.lokotro-status-warning{background:var(--lokotro-warning, #D97706);box-shadow:0 0 0 8px #d977061f,0 8px 20px #d9770640}.lokotro-status-info{background:var(--lokotro-info, #2563EB);box-shadow:0 0 0 8px #2563eb1f,0 8px 20px #2563eb40}@keyframes lokotro-pop{0%{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.lokotro-amount-focal{display:flex;flex-direction:column;align-items:center;gap:4px;margin-bottom:16px}.lokotro-amount-currency{font-size:12px;font-weight:600;letter-spacing:.08em;color:var(--lokotro-text-secondary, #64748B)}.lokotro-amount-value{font-size:40px;font-weight:700;line-height:1;color:var(--lokotro-text-primary, #0F172A);font-variant-numeric:tabular-nums}.lokotro-result-title{font-size:20px;font-weight:600;margin:0 0 6px;color:var(--lokotro-text-primary, #0F172A)}.lokotro-result-message{font-size:14px;color:var(--lokotro-text-secondary, #64748B);margin:0;line-height:1.5;max-width:320px}.lokotro-result-details{width:100%;background:var(--lokotro-surface, #F7F8FA);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:12px;padding:12px 16px;margin-top:24px}.lokotro-detail-row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:6px 0}.lokotro-detail-row:not(:last-child){border-bottom:1px solid var(--lokotro-divider, #F1F5F9)}.lokotro-detail-label{font-size:13px;color:var(--lokotro-text-secondary, #64748B)}.lokotro-detail-value{font-size:13px;font-weight:600;color:var(--lokotro-text-primary, #0F172A);text-align:right;overflow:hidden;text-overflow:ellipsis;max-width:200px;white-space:nowrap}.lokotro-mono{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Monaco,monospace;font-size:12px}.lokotro-result-actions{display:flex;flex-direction:column;gap:10px;width:100%}.lokotro-btn-primary,.lokotro-btn-secondary{width:100%;padding:14px 24px;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;transition:all .18s ease;font-family:inherit}.lokotro-btn-primary{background:var(--lokotro-primary, #0F172A);color:#fff;border:none}.lokotro-btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 16px var(--lokotro-shadow-medium, rgba(15, 23, 42, .12))}.lokotro-btn-primary:active{transform:translateY(0)}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #0F172A);border:1.5px solid var(--lokotro-border, #E2E8F0)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #64748B);background:var(--lokotro-surface, #F7F8FA)}.lokotro-countdown-indicator{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;background:var(--lokotro-surface, #F7F8FA);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:999px;cursor:pointer;margin-top:20px;transition:all .18s ease;color:var(--lokotro-text-secondary, #64748B);font-family:inherit}.lokotro-countdown-indicator:hover{background:var(--lokotro-card, #FFFFFF);border-color:var(--lokotro-text-tertiary, #64748B)}.lokotro-countdown-circle{width:14px;height:14px;transform:rotate(-90deg)}.lokotro-countdown-bg{fill:none;stroke:var(--lokotro-border, #E2E8F0);stroke-width:4}.lokotro-countdown-fill{fill:none;stroke-width:4;stroke-linecap:round;transition:stroke-dasharray .3s ease}.lokotro-countdown-success{stroke:var(--lokotro-success, #16A34A)}.lokotro-countdown-error{stroke:var(--lokotro-error, #DC2626)}.lokotro-countdown-warning{stroke:var(--lokotro-warning, #D97706)}.lokotro-countdown-info{stroke:var(--lokotro-info, #2563EB)}.lokotro-countdown-text{font-size:12px;font-weight:500}.lokotro-countdown-cancel{color:inherit;opacity:.7}\n"] }); }
|
|
3186
|
-
}
|
|
3187
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroResultComponent, decorators: [{
|
|
3188
|
-
type: Component,
|
|
3189
|
-
args: [{ selector: 'lokotro-result', standalone: true, imports: [], template: `
|
|
3190
|
-
<div class="lokotro-result" [class]="'lokotro-result-' + type">
|
|
3191
|
-
<div class="lokotro-result-card">
|
|
3192
|
-
<!-- Status Badge (filled circle with icon) -->
|
|
3193
|
-
<div class="lokotro-status-badge" [class]="'lokotro-status-' + type">
|
|
3194
|
-
@if (type === 'success') {
|
|
3195
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
3196
|
-
<polyline points="5 12 10 17 19 8"/>
|
|
3197
|
-
</svg>
|
|
3198
|
-
}
|
|
3199
|
-
@if (type === 'error') {
|
|
3200
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
3201
|
-
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
3202
|
-
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
3203
|
-
</svg>
|
|
3204
|
-
}
|
|
3205
|
-
@if (type === 'warning') {
|
|
3206
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
3207
|
-
<line x1="12" y1="8" x2="12" y2="13"/>
|
|
3208
|
-
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
3209
|
-
</svg>
|
|
3210
|
-
}
|
|
3211
|
-
@if (type === 'info') {
|
|
3212
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
3213
|
-
<line x1="12" y1="11" x2="12" y2="16"/>
|
|
3214
|
-
<line x1="12" y1="7" x2="12.01" y2="7"/>
|
|
3215
|
-
</svg>
|
|
3216
|
-
}
|
|
3217
|
-
</div>
|
|
3218
|
-
|
|
3219
|
-
<!-- Amount focal (success only, when amount provided) -->
|
|
3220
|
-
@if (type === 'success' && amount !== undefined && amount !== null) {
|
|
3221
|
-
<div class="lokotro-amount-focal">
|
|
3222
|
-
<span class="lokotro-amount-currency">{{ (currency || '').toUpperCase() }}</span>
|
|
3223
|
-
<span class="lokotro-amount-value">{{ formatNumber(amount) }}</span>
|
|
3224
|
-
</div>
|
|
2991
|
+
/**
|
|
2992
|
+
* Start polling for mobile money payment status
|
|
2993
|
+
*/
|
|
2994
|
+
startMobileMoneyPolling(transactionId) {
|
|
2995
|
+
this.stopMobileMoneyPolling();
|
|
2996
|
+
this.mobileMoneyPollingAttempts = 0;
|
|
2997
|
+
// PR-1.2: removed console.log of transactionId — operational logging
|
|
2998
|
+
// should be opt-in via merchant config, not on by default.
|
|
2999
|
+
this.mobileMoneyPollingTimer = setInterval(() => {
|
|
3000
|
+
this.mobileMoneyPollingAttempts++;
|
|
3001
|
+
if (this.mobileMoneyPollingAttempts > LokotroPaymentService.MOBILE_MONEY_MAX_ATTEMPTS) {
|
|
3002
|
+
this.stopMobileMoneyPolling();
|
|
3003
|
+
this.handlePaymentFailure('Payment took too long. Please try again or contact support.');
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
const endpoint = `${LokotroPayEnv.endpoints.mobileMoneyStatus}/${transactionId}`;
|
|
3007
|
+
this.httpClient.get(endpoint).subscribe({
|
|
3008
|
+
next: (response) => {
|
|
3009
|
+
if (!response.isSuccess || !response.data)
|
|
3010
|
+
return;
|
|
3011
|
+
const data = this.unwrapPayload(response.data);
|
|
3012
|
+
const status = this.getString(data['status']).toLowerCase();
|
|
3013
|
+
if (status === 'completed' || status === 'success' || status === 'approved') {
|
|
3014
|
+
this.stopMobileMoneyPolling();
|
|
3015
|
+
this.handlePaymentSuccess(data);
|
|
3016
|
+
}
|
|
3017
|
+
else if (status === 'failed' || status === 'error' || status === 'cancelled' || status === 'declined') {
|
|
3018
|
+
this.stopMobileMoneyPolling();
|
|
3019
|
+
this.handlePaymentFailure(this.getString(data['message']) || 'Payment failed or was declined');
|
|
3020
|
+
}
|
|
3021
|
+
},
|
|
3022
|
+
error: (_err) => {
|
|
3023
|
+
// PR-1.2: don't log full error object — may contain backend
|
|
3024
|
+
// response details that leak server internals to console.
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
}, LokotroPaymentService.MOBILE_MONEY_POLL_INTERVAL);
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* Stop mobile money status polling
|
|
3031
|
+
*/
|
|
3032
|
+
stopMobileMoneyPolling() {
|
|
3033
|
+
if (this.mobileMoneyPollingTimer) {
|
|
3034
|
+
clearInterval(this.mobileMoneyPollingTimer);
|
|
3035
|
+
this.mobileMoneyPollingTimer = undefined;
|
|
3036
|
+
this.mobileMoneyPollingAttempts = 0;
|
|
3225
3037
|
}
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3038
|
+
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Validate a server-supplied redirect URL before handing the browser tab to
|
|
3041
|
+
* it. Defense-in-depth for Angular HIGH-1 — server-side per-merchant
|
|
3042
|
+
* allow-list (PR-2.0) is the real fix.
|
|
3043
|
+
*
|
|
3044
|
+
* Policy:
|
|
3045
|
+
* - Reject non-http(s) schemes (blocks `javascript:`, `data:`, `file:`,
|
|
3046
|
+
* `intent:`, custom schemes).
|
|
3047
|
+
* - Reject http:// in production.
|
|
3048
|
+
* - Reject malformed URLs.
|
|
3049
|
+
* - SSR safety: skip the redirect when window is unavailable.
|
|
3050
|
+
*/
|
|
3051
|
+
redirectIfAllowed(url, _origin) {
|
|
3052
|
+
if (typeof window === 'undefined') {
|
|
3053
|
+
return;
|
|
3233
3054
|
}
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
<button type="button" class="lokotro-countdown-indicator" (click)="cancelCountdown()">
|
|
3238
|
-
<svg viewBox="0 0 36 36" class="lokotro-countdown-circle">
|
|
3239
|
-
<path class="lokotro-countdown-bg"
|
|
3240
|
-
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
|
3241
|
-
<path class="lokotro-countdown-fill" [class]="'lokotro-countdown-' + type"
|
|
3242
|
-
[attr.stroke-dasharray]="getCountdownProgress() + ', 100'"
|
|
3243
|
-
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
|
3244
|
-
</svg>
|
|
3245
|
-
<span class="lokotro-countdown-text">
|
|
3246
|
-
{{ localization.translate('redirectingIn', { seconds: countdownSeconds }) }}
|
|
3247
|
-
</span>
|
|
3248
|
-
<svg class="lokotro-countdown-cancel" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3249
|
-
<path d="M18 6L6 18M6 6l12 12"/>
|
|
3250
|
-
</svg>
|
|
3251
|
-
</button>
|
|
3055
|
+
if (!url || typeof url !== 'string') {
|
|
3056
|
+
this.handlePaymentFailure('Invalid redirect URL');
|
|
3057
|
+
return;
|
|
3252
3058
|
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
<div class="lokotro-result-details">
|
|
3257
|
-
<div class="lokotro-detail-row">
|
|
3258
|
-
<span class="lokotro-detail-label">Transaction ID</span>
|
|
3259
|
-
<span class="lokotro-detail-value lokotro-mono">{{ transactionId }}</span>
|
|
3260
|
-
</div>
|
|
3261
|
-
</div>
|
|
3059
|
+
let parsed;
|
|
3060
|
+
try {
|
|
3061
|
+
parsed = new URL(url, window.location.href);
|
|
3262
3062
|
}
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
</div>
|
|
3279
|
-
}
|
|
3280
|
-
</div>
|
|
3281
|
-
`, styles: [".lokotro-result{display:flex;flex-direction:column;align-items:stretch;max-width:420px;margin:0 auto;padding:24px 16px;gap:20px}.lokotro-result-card{display:flex;flex-direction:column;align-items:center;text-align:center;padding:32px 24px;background:var(--lokotro-card, #FFFFFF);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:20px;box-shadow:0 1px 3px var(--lokotro-shadow-light, rgba(15, 23, 42, .04)),0 8px 24px var(--lokotro-shadow-light, rgba(15, 23, 42, .04))}.lokotro-status-badge{width:72px;height:72px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:24px;animation:lokotro-pop .5s cubic-bezier(.34,1.56,.64,1)}.lokotro-status-badge svg{width:36px;height:36px;color:#fff}.lokotro-status-success{background:var(--lokotro-success, #16A34A);box-shadow:0 0 0 8px #16a34a1f,0 8px 20px #16a34a40}.lokotro-status-error{background:var(--lokotro-error, #DC2626);box-shadow:0 0 0 8px #dc26261f,0 8px 20px #dc262640}.lokotro-status-warning{background:var(--lokotro-warning, #D97706);box-shadow:0 0 0 8px #d977061f,0 8px 20px #d9770640}.lokotro-status-info{background:var(--lokotro-info, #2563EB);box-shadow:0 0 0 8px #2563eb1f,0 8px 20px #2563eb40}@keyframes lokotro-pop{0%{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.lokotro-amount-focal{display:flex;flex-direction:column;align-items:center;gap:4px;margin-bottom:16px}.lokotro-amount-currency{font-size:12px;font-weight:600;letter-spacing:.08em;color:var(--lokotro-text-secondary, #64748B)}.lokotro-amount-value{font-size:40px;font-weight:700;line-height:1;color:var(--lokotro-text-primary, #0F172A);font-variant-numeric:tabular-nums}.lokotro-result-title{font-size:20px;font-weight:600;margin:0 0 6px;color:var(--lokotro-text-primary, #0F172A)}.lokotro-result-message{font-size:14px;color:var(--lokotro-text-secondary, #64748B);margin:0;line-height:1.5;max-width:320px}.lokotro-result-details{width:100%;background:var(--lokotro-surface, #F7F8FA);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:12px;padding:12px 16px;margin-top:24px}.lokotro-detail-row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:6px 0}.lokotro-detail-row:not(:last-child){border-bottom:1px solid var(--lokotro-divider, #F1F5F9)}.lokotro-detail-label{font-size:13px;color:var(--lokotro-text-secondary, #64748B)}.lokotro-detail-value{font-size:13px;font-weight:600;color:var(--lokotro-text-primary, #0F172A);text-align:right;overflow:hidden;text-overflow:ellipsis;max-width:200px;white-space:nowrap}.lokotro-mono{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Monaco,monospace;font-size:12px}.lokotro-result-actions{display:flex;flex-direction:column;gap:10px;width:100%}.lokotro-btn-primary,.lokotro-btn-secondary{width:100%;padding:14px 24px;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;transition:all .18s ease;font-family:inherit}.lokotro-btn-primary{background:var(--lokotro-primary, #0F172A);color:#fff;border:none}.lokotro-btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 16px var(--lokotro-shadow-medium, rgba(15, 23, 42, .12))}.lokotro-btn-primary:active{transform:translateY(0)}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #0F172A);border:1.5px solid var(--lokotro-border, #E2E8F0)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #64748B);background:var(--lokotro-surface, #F7F8FA)}.lokotro-countdown-indicator{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;background:var(--lokotro-surface, #F7F8FA);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:999px;cursor:pointer;margin-top:20px;transition:all .18s ease;color:var(--lokotro-text-secondary, #64748B);font-family:inherit}.lokotro-countdown-indicator:hover{background:var(--lokotro-card, #FFFFFF);border-color:var(--lokotro-text-tertiary, #64748B)}.lokotro-countdown-circle{width:14px;height:14px;transform:rotate(-90deg)}.lokotro-countdown-bg{fill:none;stroke:var(--lokotro-border, #E2E8F0);stroke-width:4}.lokotro-countdown-fill{fill:none;stroke-width:4;stroke-linecap:round;transition:stroke-dasharray .3s ease}.lokotro-countdown-success{stroke:var(--lokotro-success, #16A34A)}.lokotro-countdown-error{stroke:var(--lokotro-error, #DC2626)}.lokotro-countdown-warning{stroke:var(--lokotro-warning, #D97706)}.lokotro-countdown-info{stroke:var(--lokotro-info, #2563EB)}.lokotro-countdown-text{font-size:12px;font-weight:500}.lokotro-countdown-cancel{color:inherit;opacity:.7}\n"] }]
|
|
3282
|
-
}], ctorParameters: () => [{ type: LokotroLocalizationService }], propDecorators: { type: [{
|
|
3283
|
-
type: Input
|
|
3284
|
-
}], title: [{
|
|
3285
|
-
type: Input
|
|
3286
|
-
}], message: [{
|
|
3287
|
-
type: Input
|
|
3288
|
-
}], amount: [{
|
|
3289
|
-
type: Input
|
|
3290
|
-
}], currency: [{
|
|
3291
|
-
type: Input
|
|
3292
|
-
}], transactionId: [{
|
|
3293
|
-
type: Input
|
|
3294
|
-
}], primaryActionLabel: [{
|
|
3295
|
-
type: Input
|
|
3296
|
-
}], secondaryActionLabel: [{
|
|
3297
|
-
type: Input
|
|
3298
|
-
}], autoRedirectSeconds: [{
|
|
3299
|
-
type: Input
|
|
3300
|
-
}], primaryAction: [{
|
|
3301
|
-
type: Output
|
|
3302
|
-
}], secondaryAction: [{
|
|
3303
|
-
type: Output
|
|
3304
|
-
}], autoRedirect: [{
|
|
3305
|
-
type: Output
|
|
3306
|
-
}] } });
|
|
3307
|
-
|
|
3308
|
-
/**
|
|
3309
|
-
* Lokotro Pay - Loading Component
|
|
3310
|
-
*/
|
|
3311
|
-
class LokotroLoadingComponent {
|
|
3312
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroLoadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3313
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: LokotroLoadingComponent, isStandalone: true, selector: "lokotro-loading", inputs: { message: "message" }, ngImport: i0, template: `
|
|
3314
|
-
<div class="lokotro-loading">
|
|
3315
|
-
<div class="lokotro-loading-spinner">
|
|
3316
|
-
<div class="lokotro-pulse-ring"></div>
|
|
3317
|
-
<div class="lokotro-pulse-ring"></div>
|
|
3318
|
-
<div class="lokotro-pulse-dot"></div>
|
|
3319
|
-
</div>
|
|
3320
|
-
@if (message) {
|
|
3321
|
-
<p class="lokotro-loading-message">{{ message }}</p>
|
|
3322
|
-
}
|
|
3323
|
-
</div>
|
|
3324
|
-
`, isInline: true, styles: [".lokotro-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;padding:24px}.lokotro-loading-spinner{position:relative;width:60px;height:60px;margin-bottom:24px}.lokotro-pulse-ring{position:absolute;width:100%;height:100%;border:3px solid var(--lokotro-accent, #3BFBDA);border-radius:50%;opacity:0;animation:lokotro-pulse 2s cubic-bezier(.215,.61,.355,1) infinite}.lokotro-pulse-ring:nth-child(2){animation-delay:.5s}.lokotro-pulse-dot{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:16px;height:16px;background:var(--lokotro-accent, #3BFBDA);border-radius:50%;animation:lokotro-dot-pulse 2s ease-in-out infinite}@keyframes lokotro-pulse{0%{transform:scale(.5);opacity:.8}to{transform:scale(1.5);opacity:0}}@keyframes lokotro-dot-pulse{0%,to{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.2)}}.lokotro-loading-message{font-size:16px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0}\n"] }); }
|
|
3325
|
-
}
|
|
3326
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroLoadingComponent, decorators: [{
|
|
3327
|
-
type: Component,
|
|
3328
|
-
args: [{ selector: 'lokotro-loading', standalone: true, imports: [], template: `
|
|
3329
|
-
<div class="lokotro-loading">
|
|
3330
|
-
<div class="lokotro-loading-spinner">
|
|
3331
|
-
<div class="lokotro-pulse-ring"></div>
|
|
3332
|
-
<div class="lokotro-pulse-ring"></div>
|
|
3333
|
-
<div class="lokotro-pulse-dot"></div>
|
|
3334
|
-
</div>
|
|
3335
|
-
@if (message) {
|
|
3336
|
-
<p class="lokotro-loading-message">{{ message }}</p>
|
|
3337
|
-
}
|
|
3338
|
-
</div>
|
|
3339
|
-
`, styles: [".lokotro-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;padding:24px}.lokotro-loading-spinner{position:relative;width:60px;height:60px;margin-bottom:24px}.lokotro-pulse-ring{position:absolute;width:100%;height:100%;border:3px solid var(--lokotro-accent, #3BFBDA);border-radius:50%;opacity:0;animation:lokotro-pulse 2s cubic-bezier(.215,.61,.355,1) infinite}.lokotro-pulse-ring:nth-child(2){animation-delay:.5s}.lokotro-pulse-dot{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:16px;height:16px;background:var(--lokotro-accent, #3BFBDA);border-radius:50%;animation:lokotro-dot-pulse 2s ease-in-out infinite}@keyframes lokotro-pulse{0%{transform:scale(.5);opacity:.8}to{transform:scale(1.5);opacity:0}}@keyframes lokotro-dot-pulse{0%,to{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.2)}}.lokotro-loading-message{font-size:16px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0}\n"] }]
|
|
3340
|
-
}], propDecorators: { message: [{
|
|
3341
|
-
type: Input
|
|
3342
|
-
}] } });
|
|
3343
|
-
|
|
3344
|
-
/**
|
|
3345
|
-
* Lokotro Pay - Feature Flags
|
|
3346
|
-
*
|
|
3347
|
-
* Compile-time switches for capabilities that are partially wired into the SDK
|
|
3348
|
-
* but not yet ready to expose to end users. Flip a flag to `true` once the
|
|
3349
|
-
* feature is implemented end-to-end.
|
|
3350
|
-
*/
|
|
3351
|
-
const LOKOTRO_PAY_FEATURE_FLAGS = {
|
|
3063
|
+
catch {
|
|
3064
|
+
this.handlePaymentFailure('Untrusted redirect URL');
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
3068
|
+
this.handlePaymentFailure('Untrusted redirect scheme');
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
const isProduction = LokotroPayEnv.environment === 'production';
|
|
3072
|
+
if (isProduction && parsed.protocol !== 'https:') {
|
|
3073
|
+
this.handlePaymentFailure('Insecure redirect rejected');
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
window.location.href = parsed.toString();
|
|
3077
|
+
}
|
|
3352
3078
|
/**
|
|
3353
|
-
*
|
|
3354
|
-
*
|
|
3355
|
-
* The models and method-flag plumbing already understand these methods, but
|
|
3356
|
-
* the native tokenisation flows are not implemented end-to-end yet. While
|
|
3357
|
-
* this is `false` the SDK hides Google Pay / Apple Pay from the
|
|
3358
|
-
* payment-method selection UI so they can never be shown or selected. Set to
|
|
3359
|
-
* `true` once the native integrations are complete.
|
|
3079
|
+
* Handle payment success
|
|
3360
3080
|
*/
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
}
|
|
3382
|
-
/**
|
|
3383
|
-
* Returns the payment methods that should be visible to the user, dropping any
|
|
3384
|
-
* hidden behind a disabled feature flag (currently Google / Apple Pay). The
|
|
3385
|
-
* list is returned unchanged once the relevant flag is enabled.
|
|
3386
|
-
*/
|
|
3387
|
-
function visiblePaymentMethods(methods) {
|
|
3388
|
-
if (LOKOTRO_PAY_FEATURE_FLAGS.nativeWalletsEnabled) {
|
|
3389
|
-
return methods;
|
|
3390
|
-
}
|
|
3391
|
-
return methods.filter((method) => !isNativeWalletMethod(method));
|
|
3392
|
-
}
|
|
3393
|
-
|
|
3394
|
-
/**
|
|
3395
|
-
* Lokotro Pay - Payment Service
|
|
3396
|
-
* Main payment service that follows the Lokotro Gateway API flow
|
|
3397
|
-
*/
|
|
3398
|
-
/**
|
|
3399
|
-
* Initial payment state
|
|
3400
|
-
*/
|
|
3401
|
-
const initialState = {
|
|
3402
|
-
isLoading: false,
|
|
3403
|
-
currentScreen: LokotroPayScreenNavigation.LoadingScreen
|
|
3404
|
-
};
|
|
3405
|
-
class LokotroPaymentService {
|
|
3406
|
-
static { this.MOBILE_MONEY_POLL_INTERVAL = 5000; }
|
|
3407
|
-
static { this.MOBILE_MONEY_MAX_ATTEMPTS = 60; }
|
|
3408
|
-
constructor(httpClient) {
|
|
3409
|
-
this.httpClient = httpClient;
|
|
3410
|
-
this.stateSubject = new BehaviorSubject(initialState);
|
|
3411
|
-
this.state$ = this.stateSubject.asObservable();
|
|
3412
|
-
this.mobileMoneyPollingAttempts = 0;
|
|
3081
|
+
handlePaymentSuccess(data, paymentStatus = LokotroPaymentStatus.Approved, defaultTitle = 'Success') {
|
|
3082
|
+
this.stopMobileMoneyPolling();
|
|
3083
|
+
const transactionId = this.getString(data['transaction_id']) || this.getString(data['payment_id']);
|
|
3084
|
+
const response = {
|
|
3085
|
+
message: this.getString(data['message']) || 'Payment successful',
|
|
3086
|
+
title: this.getString(data['title']) || defaultTitle,
|
|
3087
|
+
customRef: this.getString(data['custom_ref']) || this.getString(data['customer_reference']),
|
|
3088
|
+
amount: this.getNumber(data['amount']),
|
|
3089
|
+
apiResponseCode: LokotroPayApiResponseCode.LOK000,
|
|
3090
|
+
currency: this.getString(data['currency']) || this.getString(data['currency_str']),
|
|
3091
|
+
paymentStatus,
|
|
3092
|
+
systemRef: this.getString(data['system_reference']) || this.getString(data['system_ref']) || transactionId,
|
|
3093
|
+
transactionId,
|
|
3094
|
+
timestamp: new Date()
|
|
3095
|
+
};
|
|
3096
|
+
this.updateState({
|
|
3097
|
+
isLoading: false,
|
|
3098
|
+
response,
|
|
3099
|
+
currentScreen: LokotroPayScreenNavigation.SuccessScreen
|
|
3100
|
+
});
|
|
3413
3101
|
}
|
|
3414
3102
|
/**
|
|
3415
|
-
*
|
|
3103
|
+
* Handle payment failure
|
|
3416
3104
|
*/
|
|
3417
|
-
|
|
3418
|
-
|
|
3105
|
+
handlePaymentFailure(message, errorCode) {
|
|
3106
|
+
this.stopMobileMoneyPolling();
|
|
3107
|
+
this.updateState({
|
|
3108
|
+
isLoading: false,
|
|
3109
|
+
error: {
|
|
3110
|
+
message: message || 'Payment failed',
|
|
3111
|
+
title: 'Payment Failed',
|
|
3112
|
+
errorCode,
|
|
3113
|
+
timestamp: new Date()
|
|
3114
|
+
},
|
|
3115
|
+
currentScreen: LokotroPayScreenNavigation.ErrorScreen
|
|
3116
|
+
});
|
|
3419
3117
|
}
|
|
3420
3118
|
/**
|
|
3421
|
-
*
|
|
3119
|
+
* Resolve the collect response into the same flow the Flutter SDK uses.
|
|
3422
3120
|
*/
|
|
3423
|
-
|
|
3424
|
-
this.
|
|
3425
|
-
|
|
3426
|
-
|
|
3121
|
+
resolveCollectResponse(collectData, paymentBody) {
|
|
3122
|
+
const paymentId = this.resolvePaymentId(collectData);
|
|
3123
|
+
const paymentStatus = this.getString(collectData['status']).toLowerCase();
|
|
3124
|
+
const paymentInfo = this.parsePaymentInfo(collectData);
|
|
3125
|
+
const additionalData = this.asRecord(collectData['additional_data']);
|
|
3126
|
+
// Bank-transfer switch offer: the amount is out of range for the chosen
|
|
3127
|
+
// method but fits bank transfer. Pre-transaction (no id yet) — detect it
|
|
3128
|
+
// before any status routing. NOT a silent switch: route to the confirm
|
|
3129
|
+
// screen; the customer accepts (confirmBankTransferSwitch → re-collect) or
|
|
3130
|
+
// declines (declineBankTransferSwitch → method selection).
|
|
3131
|
+
const bankTransferOffer = this.asRecord(additionalData?.['bank_transfer_offer']);
|
|
3132
|
+
if (bankTransferOffer && this.getBoolean(bankTransferOffer['available'])) {
|
|
3133
|
+
this.updateState({
|
|
3134
|
+
isLoading: false,
|
|
3135
|
+
bankTransferOffer,
|
|
3136
|
+
currentScreen: LokotroPayScreenNavigation.BankTransferOfferScreen
|
|
3137
|
+
});
|
|
3138
|
+
return of(void 0);
|
|
3139
|
+
}
|
|
3140
|
+
if (paymentStatus === 'pending_otp' || paymentStatus === 'pending_otp_verification') {
|
|
3141
|
+
this.updateState({
|
|
3142
|
+
isLoading: false,
|
|
3143
|
+
transactionId: paymentId,
|
|
3144
|
+
paymentInfo,
|
|
3145
|
+
currentScreen: LokotroPayScreenNavigation.EWalletOtpScreen
|
|
3146
|
+
});
|
|
3147
|
+
return of(void 0);
|
|
3148
|
+
}
|
|
3149
|
+
if (paymentStatus === 'pending_bank_proof_upload') {
|
|
3150
|
+
this.handlePaymentSuccess(collectData, LokotroPaymentStatus.PendingBankProofUpload, 'Upload Proof Required');
|
|
3151
|
+
return of(void 0);
|
|
3152
|
+
}
|
|
3153
|
+
if (paymentStatus === 'completed' || paymentStatus === 'approved' || paymentStatus === 'success') {
|
|
3154
|
+
this.handlePaymentSuccess(collectData);
|
|
3155
|
+
return of(void 0);
|
|
3156
|
+
}
|
|
3157
|
+
if (paymentStatus === 'session_created') {
|
|
3158
|
+
const redirectUrl = this.getString(collectData['mastercardUrl']) ||
|
|
3159
|
+
this.getString(collectData['redirect_url']) ||
|
|
3160
|
+
this.getString(collectData['payment_url']);
|
|
3161
|
+
if (!redirectUrl) {
|
|
3162
|
+
this.handlePaymentFailure('Hosted session created but no checkout URL was provided');
|
|
3163
|
+
return of(void 0);
|
|
3164
|
+
}
|
|
3165
|
+
this.updateState({
|
|
3166
|
+
isLoading: false,
|
|
3167
|
+
transactionId: paymentId
|
|
3168
|
+
});
|
|
3169
|
+
// PR-1.2 / Angular HIGH-1: validate before redirect.
|
|
3170
|
+
this.redirectIfAllowed(redirectUrl, 'session-created');
|
|
3171
|
+
return of(void 0);
|
|
3172
|
+
}
|
|
3173
|
+
if (this.getString(additionalData?.['action']) === 'select_payment_method') {
|
|
3174
|
+
this.updateState({
|
|
3175
|
+
isLoading: false,
|
|
3176
|
+
transactionId: paymentId,
|
|
3177
|
+
paymentInfo,
|
|
3178
|
+
currentScreen: LokotroPayScreenNavigation.PaymentMethodSelectionScreen
|
|
3179
|
+
});
|
|
3180
|
+
return of(void 0);
|
|
3181
|
+
}
|
|
3182
|
+
if (paymentStatus === 'failed' || paymentStatus === 'rejected' || paymentStatus === 'declined') {
|
|
3183
|
+
this.handlePaymentFailure(this.getString(collectData['message']) || 'Payment failed');
|
|
3184
|
+
return of(void 0);
|
|
3185
|
+
}
|
|
3186
|
+
if (!paymentId) {
|
|
3187
|
+
this.handlePaymentFailure('Transaction ID not received from payment collection');
|
|
3188
|
+
return of(void 0);
|
|
3189
|
+
}
|
|
3190
|
+
return this.getTransactionDetails(paymentId).pipe(switchMap(response => this.resolveTransactionResponse(response, paymentBody)));
|
|
3191
|
+
}
|
|
3192
|
+
/**
|
|
3193
|
+
* Resolve transaction details into the correct screen or auto-submit flow.
|
|
3194
|
+
*/
|
|
3195
|
+
resolveTransactionResponse(response, paymentBody) {
|
|
3196
|
+
if (!response.isSuccess || !response.data) {
|
|
3197
|
+
this.handlePaymentFailure(response.message || 'Failed to load transaction details', response.apiResponseCode);
|
|
3198
|
+
return of(void 0);
|
|
3199
|
+
}
|
|
3200
|
+
const transactionData = this.unwrapPayload(response.data);
|
|
3201
|
+
const paymentId = this.resolvePaymentId(transactionData);
|
|
3202
|
+
if (!paymentId) {
|
|
3203
|
+
this.handlePaymentFailure('Transaction ID not found in transaction details');
|
|
3204
|
+
return of(void 0);
|
|
3205
|
+
}
|
|
3206
|
+
const paymentInfo = this.parseTransactionPaymentInfo(transactionData);
|
|
3207
|
+
const selectedPaymentMethod = this.resolveSelectedPaymentMethod(transactionData, paymentInfo);
|
|
3208
|
+
if (!paymentInfo.showUserInfoForm && !paymentInfo.showPaymentMethodForm) {
|
|
3209
|
+
this.updateState({
|
|
3210
|
+
transactionId: paymentId,
|
|
3211
|
+
paymentInfo,
|
|
3212
|
+
selectedPaymentMethod
|
|
3213
|
+
});
|
|
3214
|
+
if (selectedPaymentMethod?.channel === LokotroPayChannel.Card) {
|
|
3215
|
+
const hostedCheckoutUrl = this.getString(transactionData['mastercardUrl']) ||
|
|
3216
|
+
this.getString(transactionData['redirect_url']);
|
|
3217
|
+
if (hostedCheckoutUrl) {
|
|
3218
|
+
this.updateState({ isLoading: false });
|
|
3219
|
+
// PR-1.2 / Angular HIGH-1: validate hosted-checkout URL.
|
|
3220
|
+
this.redirectIfAllowed(hostedCheckoutUrl, 'hosted-checkout');
|
|
3221
|
+
return of(void 0);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
const paymentMethodId = selectedPaymentMethod?.id || this.getString(transactionData['payment_method_id']);
|
|
3225
|
+
if (!paymentMethodId) {
|
|
3226
|
+
this.handlePaymentFailure('Payment method not found for auto-processing');
|
|
3227
|
+
return of(void 0);
|
|
3228
|
+
}
|
|
3229
|
+
return this.submitPaymentDetails(this.buildAutoSubmitRequest(paymentId, paymentMethodId, paymentBody)).pipe(map(() => void 0));
|
|
3230
|
+
}
|
|
3231
|
+
const nextScreen = paymentInfo.showPaymentMethodForm
|
|
3232
|
+
? LokotroPayScreenNavigation.PaymentMethodSelectionScreen
|
|
3233
|
+
: this.getFormScreenForChannel(selectedPaymentMethod?.channel || this.parseChannel(this.getString(transactionData['channel'])));
|
|
3234
|
+
this.updateState({
|
|
3235
|
+
isLoading: false,
|
|
3236
|
+
transactionId: paymentId,
|
|
3237
|
+
paymentInfo,
|
|
3238
|
+
selectedPaymentMethod,
|
|
3239
|
+
currentScreen: nextScreen
|
|
3427
3240
|
});
|
|
3241
|
+
return of(void 0);
|
|
3428
3242
|
}
|
|
3429
3243
|
/**
|
|
3430
|
-
*
|
|
3244
|
+
* Get form screen for payment channel
|
|
3431
3245
|
*/
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3246
|
+
getFormScreenForChannel(channel) {
|
|
3247
|
+
switch (channel) {
|
|
3248
|
+
case LokotroPayChannel.EWallet:
|
|
3249
|
+
case LokotroPayChannel.LokotroWallet:
|
|
3250
|
+
return LokotroPayScreenNavigation.EWalletFormScreen;
|
|
3251
|
+
case LokotroPayChannel.MobileMoney:
|
|
3252
|
+
return LokotroPayScreenNavigation.MobileMoneyFormScreen;
|
|
3253
|
+
case LokotroPayChannel.Card:
|
|
3254
|
+
case LokotroPayChannel.VirtualCard:
|
|
3255
|
+
return LokotroPayScreenNavigation.CardFormScreen;
|
|
3256
|
+
case LokotroPayChannel.EFlash:
|
|
3257
|
+
return LokotroPayScreenNavigation.FlashFormScreen;
|
|
3258
|
+
case LokotroPayChannel.BankTransfer:
|
|
3259
|
+
return LokotroPayScreenNavigation.BankTransferFormScreen;
|
|
3260
|
+
default:
|
|
3261
|
+
return LokotroPayScreenNavigation.PaymentFormScreen;
|
|
3262
|
+
}
|
|
3436
3263
|
}
|
|
3437
3264
|
/**
|
|
3438
|
-
*
|
|
3265
|
+
* Fetch the banks / accounts / entities available for a bank-transfer
|
|
3266
|
+
* selection. Calls the gateway's /payments/get-config-bank (the same endpoint
|
|
3267
|
+
* the Flutter SDK uses) and maps the snake_case payload into the typed models.
|
|
3268
|
+
* Returns a LokotroHttpResponse<LokotroBank[]> so the bank form can read
|
|
3269
|
+
* response.isSuccess + response.data.
|
|
3439
3270
|
*/
|
|
3440
|
-
|
|
3441
|
-
this.httpClient.
|
|
3271
|
+
fetchAvailableBanks() {
|
|
3272
|
+
return this.httpClient.get('/payments/get-config-bank').pipe(map(response => {
|
|
3273
|
+
const rows = this.asArray(response.data?.['data']);
|
|
3274
|
+
return {
|
|
3275
|
+
isSuccess: response.isSuccess,
|
|
3276
|
+
message: response.message,
|
|
3277
|
+
data: rows.map(row => this.mapBank(row))
|
|
3278
|
+
};
|
|
3279
|
+
}));
|
|
3442
3280
|
}
|
|
3443
3281
|
/**
|
|
3444
|
-
*
|
|
3282
|
+
* Customer accepted the bank-transfer switch offer — re-collect with the
|
|
3283
|
+
* method forced to bank_transfer (analogue of the Flutter re-init). Clears the
|
|
3284
|
+
* offer first so the confirm screen doesn't flash again.
|
|
3445
3285
|
*/
|
|
3446
|
-
|
|
3447
|
-
this.
|
|
3286
|
+
confirmBankTransferSwitch() {
|
|
3287
|
+
const body = this.currentPaymentBody;
|
|
3288
|
+
if (!body) {
|
|
3289
|
+
this.handlePaymentFailure('Cannot switch to bank transfer: no payment in progress');
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3292
|
+
this.updateState({ bankTransferOffer: null });
|
|
3293
|
+
this.createPayment({ ...body, paymentMethod: 'bank_transfer' }).subscribe();
|
|
3294
|
+
}
|
|
3295
|
+
/** Customer declined the switch — return to payment-method selection. */
|
|
3296
|
+
declineBankTransferSwitch() {
|
|
3297
|
+
this.updateState({ bankTransferOffer: null });
|
|
3298
|
+
this.navigateToScreen(LokotroPayScreenNavigation.PaymentMethodSelectionScreen);
|
|
3299
|
+
}
|
|
3300
|
+
asArray(value) {
|
|
3301
|
+
return Array.isArray(value) ? value : [];
|
|
3302
|
+
}
|
|
3303
|
+
mapBank(row) {
|
|
3304
|
+
return {
|
|
3305
|
+
id: this.getString(row['id']),
|
|
3306
|
+
name: this.getString(row['name']),
|
|
3307
|
+
abreviation: this.getString(row['abreviation']),
|
|
3308
|
+
bankLogoUrl: this.getString(row['bank_logo_url']),
|
|
3309
|
+
hasRibNomenclatureConstraint: this.getBoolean(row['has_rib_nomenclature_constraint']),
|
|
3310
|
+
ribAccountNumberFormatStr: this.getString(row['rib_account_number_format_str']),
|
|
3311
|
+
bankAccounts: this.asArray(row['bank_accounts']).map(a => this.mapBankAccount(a)),
|
|
3312
|
+
entityAvailables: this.asArray(row['entity_availables']).map(e => this.mapBankEntity(e))
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
mapBankAccount(row) {
|
|
3316
|
+
const refBank = this.asRecord(row['ref_bank']);
|
|
3317
|
+
const refCurrency = this.asRecord(row['ref_currency']);
|
|
3318
|
+
return {
|
|
3319
|
+
id: this.getString(row['id']),
|
|
3320
|
+
identifier: this.getString(row['identifier']) || this.getString(row['id']),
|
|
3321
|
+
accountNumber: this.getString(row['account_number']),
|
|
3322
|
+
accountLabel: this.getString(row['account_label']),
|
|
3323
|
+
createdAt: this.getString(row['created_at']),
|
|
3324
|
+
refBank: refBank
|
|
3325
|
+
? {
|
|
3326
|
+
id: this.getString(refBank['id']),
|
|
3327
|
+
name: this.getString(refBank['name']),
|
|
3328
|
+
abreviation: this.getString(refBank['abreviation']),
|
|
3329
|
+
bankLogoUrl: this.getString(refBank['logo_url']),
|
|
3330
|
+
hasRibNomenclatureConstraint: false,
|
|
3331
|
+
ribAccountNumberFormatStr: '',
|
|
3332
|
+
bankAccounts: [],
|
|
3333
|
+
entityAvailables: []
|
|
3334
|
+
}
|
|
3335
|
+
: undefined,
|
|
3336
|
+
refCurrency: refCurrency ? this.mapBankCurrency(refCurrency) : undefined
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
mapBankEntity(row) {
|
|
3340
|
+
return {
|
|
3341
|
+
id: this.getString(row['id']),
|
|
3342
|
+
name: this.getString(row['name']),
|
|
3343
|
+
countryFlag: this.getString(row['country_flag'])
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
mapBankCurrency(row) {
|
|
3347
|
+
return {
|
|
3348
|
+
id: this.getString(row['id']),
|
|
3349
|
+
name: this.getString(row['name']),
|
|
3350
|
+
code: this.getString(row['code']),
|
|
3351
|
+
symbol: this.getString(row['symbol'])
|
|
3352
|
+
};
|
|
3448
3353
|
}
|
|
3449
3354
|
/**
|
|
3450
|
-
*
|
|
3451
|
-
* POST /payments/collect
|
|
3355
|
+
* Convert payment body to API request format
|
|
3452
3356
|
*/
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3357
|
+
convertPaymentBodyToRequest(body) {
|
|
3358
|
+
const request = {
|
|
3359
|
+
payment_method: body.paymentMethod || 'wallet',
|
|
3360
|
+
user_info: body.userInfo || 'full',
|
|
3361
|
+
payment_method_info: body.paymentMethodInfo || 'full',
|
|
3362
|
+
fee_covered_by: body.feeCoveredBy || 'buyer',
|
|
3363
|
+
delivery_behaviour: body.deliveryBehaviour || 'direct_delivery'
|
|
3364
|
+
};
|
|
3365
|
+
// Auth-v3 refactor: customer_reference / amount / currency are bound
|
|
3366
|
+
// on the session_token. The gateway reads them straight from the
|
|
3367
|
+
// session — sending them again on the wire is redundant, and was
|
|
3368
|
+
// the source of the `"1.00"` vs `"1"` formatting drift that 400'd
|
|
3369
|
+
// body-binding. Only include them when the merchant explicitly set
|
|
3370
|
+
// them (legacy / display reasons). The gateway treats them as a
|
|
3371
|
+
// soft check when present, ignores when absent.
|
|
3372
|
+
if (body.customerReference)
|
|
3373
|
+
request['customer_reference'] = body.customerReference;
|
|
3374
|
+
if (body.amount)
|
|
3375
|
+
request['amount'] = body.amount;
|
|
3376
|
+
if (body.currency)
|
|
3377
|
+
request['currency'] = body.currency.toLowerCase();
|
|
3378
|
+
if (body.notifyUrl)
|
|
3379
|
+
request['notify_url'] = body.notifyUrl;
|
|
3380
|
+
if (body.successRedirectUrl)
|
|
3381
|
+
request['success_redirect_url'] = body.successRedirectUrl;
|
|
3382
|
+
if (body.failRedirectUrl)
|
|
3383
|
+
request['fail_redirect_url'] = body.failRedirectUrl;
|
|
3384
|
+
if (body.userInfo === 'full') {
|
|
3385
|
+
if (body.firstName)
|
|
3386
|
+
request['first_name'] = body.firstName;
|
|
3387
|
+
if (body.lastName)
|
|
3388
|
+
request['last_name'] = body.lastName;
|
|
3389
|
+
if (body.phoneNumber)
|
|
3390
|
+
request['phone_number'] = body.phoneNumber;
|
|
3391
|
+
if (body.email)
|
|
3392
|
+
request['email'] = body.email;
|
|
3393
|
+
}
|
|
3394
|
+
if (body.paymentMethodInfo === 'full') {
|
|
3395
|
+
if (body.walletNumber)
|
|
3396
|
+
request['wallet_number'] = body.walletNumber;
|
|
3397
|
+
if (body.walletPin)
|
|
3398
|
+
request['wallet_pin'] = body.walletPin;
|
|
3399
|
+
if (body.mobileMoneyPhoneNumber)
|
|
3400
|
+
request['mobile_money_phone_number'] = body.mobileMoneyPhoneNumber;
|
|
3401
|
+
if (body.flashNumber)
|
|
3402
|
+
request['flash_number'] = body.flashNumber;
|
|
3403
|
+
if (body.flashPin)
|
|
3404
|
+
request['flash_pin'] = body.flashPin;
|
|
3405
|
+
if (body.cardNumber)
|
|
3406
|
+
request['card_number'] = body.cardNumber;
|
|
3407
|
+
if (body.cardExpiryDate)
|
|
3408
|
+
request['card_expiry_date'] = body.cardExpiryDate;
|
|
3409
|
+
if (body.cardCvv)
|
|
3410
|
+
request['card_cvv'] = body.cardCvv;
|
|
3411
|
+
if (body.cardHolderName)
|
|
3412
|
+
request['card_holder_name'] = body.cardHolderName;
|
|
3413
|
+
}
|
|
3414
|
+
if (body.merchant) {
|
|
3415
|
+
request['merchant'] = {
|
|
3416
|
+
name: body.merchant.name,
|
|
3417
|
+
url: body.merchant.website,
|
|
3418
|
+
logo: body.merchant.logoUrl
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
if (body.mastercardPaymentMethod) {
|
|
3422
|
+
request['mastercard_payment_method'] = body.mastercardPaymentMethod;
|
|
3423
|
+
}
|
|
3424
|
+
if (body.transactionalCurrency) {
|
|
3425
|
+
request['transactional_currency'] = body.transactionalCurrency.toLowerCase();
|
|
3426
|
+
}
|
|
3427
|
+
if (body.nativePayToken) {
|
|
3428
|
+
request['native_pay_token'] = body.nativePayToken;
|
|
3429
|
+
}
|
|
3430
|
+
if (body.nativePayTokenType) {
|
|
3431
|
+
request['native_pay_token_type'] = body.nativePayTokenType;
|
|
3432
|
+
}
|
|
3433
|
+
if (body.metadata) {
|
|
3434
|
+
request['metadata'] = body.metadata;
|
|
3435
|
+
}
|
|
3436
|
+
return request;
|
|
3489
3437
|
}
|
|
3490
3438
|
/**
|
|
3491
|
-
*
|
|
3492
|
-
* GET /payments/transaction/{transaction_id}
|
|
3439
|
+
* Convert submit request to API format
|
|
3493
3440
|
*/
|
|
3494
|
-
|
|
3495
|
-
|
|
3441
|
+
convertSubmitRequestToData(request) {
|
|
3442
|
+
const data = {
|
|
3443
|
+
payment_id: this.resolvePaymentId(request),
|
|
3444
|
+
payment_method_id: request.paymentMethodId || request.paymentMethod || ''
|
|
3445
|
+
};
|
|
3446
|
+
if (request.firstName)
|
|
3447
|
+
data['first_name'] = request.firstName;
|
|
3448
|
+
if (request.lastName)
|
|
3449
|
+
data['last_name'] = request.lastName;
|
|
3450
|
+
if (request.email)
|
|
3451
|
+
data['email'] = request.email;
|
|
3452
|
+
if (request.phoneNumber)
|
|
3453
|
+
data['phone_number'] = request.phoneNumber;
|
|
3454
|
+
if (request.walletNumber)
|
|
3455
|
+
data['ewallet_number'] = request.walletNumber;
|
|
3456
|
+
if (request.walletPin)
|
|
3457
|
+
data['ewallet_pin'] = request.walletPin;
|
|
3458
|
+
if (request.mobileMoneyPhoneNumber)
|
|
3459
|
+
data['mobile_money_phone_number'] = request.mobileMoneyPhoneNumber;
|
|
3460
|
+
if (request.flashNumber)
|
|
3461
|
+
data['flash_number'] = request.flashNumber;
|
|
3462
|
+
if (request.flashPin)
|
|
3463
|
+
data['flash_pin'] = request.flashPin;
|
|
3464
|
+
if (request.cardNumber)
|
|
3465
|
+
data['card_number'] = request.cardNumber;
|
|
3466
|
+
if (request.cardExpiryDate)
|
|
3467
|
+
data['card_expiry_date'] = request.cardExpiryDate;
|
|
3468
|
+
if (request.cardCvv)
|
|
3469
|
+
data['card_cvv'] = request.cardCvv;
|
|
3470
|
+
if (request.cardHolderName)
|
|
3471
|
+
data['card_holder_name'] = request.cardHolderName;
|
|
3472
|
+
if (request.bankTransferAccountId)
|
|
3473
|
+
data['bank_account_id'] = request.bankTransferAccountId;
|
|
3474
|
+
if (request.bankAccountId)
|
|
3475
|
+
data['bank_account_id'] = request.bankAccountId;
|
|
3476
|
+
if (request.bankAccountNumber)
|
|
3477
|
+
data['bank_account_number'] = request.bankAccountNumber;
|
|
3478
|
+
if (request.bankAccountLabel)
|
|
3479
|
+
data['bank_account_label'] = request.bankAccountLabel;
|
|
3480
|
+
if (request.bankId)
|
|
3481
|
+
data['bank_id'] = request.bankId;
|
|
3482
|
+
if (request.bankEntityId)
|
|
3483
|
+
data['bank_entity_id'] = request.bankEntityId;
|
|
3484
|
+
if (request.mastercardPaymentMethod)
|
|
3485
|
+
data['mastercard_payment_method'] = request.mastercardPaymentMethod;
|
|
3486
|
+
if (request.sessionId)
|
|
3487
|
+
data['session_id'] = request.sessionId;
|
|
3488
|
+
if (request.cardData)
|
|
3489
|
+
data['card_data'] = request.cardData;
|
|
3490
|
+
if (typeof request.savePaymentMethod === 'boolean')
|
|
3491
|
+
data['save_payment_method'] = request.savePaymentMethod;
|
|
3492
|
+
return data;
|
|
3496
3493
|
}
|
|
3497
3494
|
/**
|
|
3498
|
-
*
|
|
3499
|
-
* POST /payments/submit
|
|
3495
|
+
* Parse payment info from collect response.
|
|
3500
3496
|
*/
|
|
3501
|
-
|
|
3502
|
-
this.
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
});
|
|
3524
|
-
// PR-1.2 / Angular HIGH-1: validate redirect URL against allow-list
|
|
3525
|
-
// before handing the tab over. Server-side allow-list (PR-2.0) is
|
|
3526
|
-
// the real defense; this is defense-in-depth on the client.
|
|
3527
|
-
this.redirectIfAllowed(submitResponse.redirectUrl, 'submit-response');
|
|
3528
|
-
return;
|
|
3529
|
-
}
|
|
3530
|
-
if (submitResponse.status === LokotroPaymentStatus.PendingBankProofUpload) {
|
|
3531
|
-
this.handlePaymentSuccess(responseData, LokotroPaymentStatus.PendingBankProofUpload, 'Upload Proof Required');
|
|
3532
|
-
return;
|
|
3533
|
-
}
|
|
3534
|
-
if (LokotroPaymentStatusInfo.isSuccess(submitResponse.status)) {
|
|
3535
|
-
this.handlePaymentSuccess(responseData);
|
|
3536
|
-
return;
|
|
3537
|
-
}
|
|
3538
|
-
if (LokotroPaymentStatusInfo.isPending(submitResponse.status)) {
|
|
3539
|
-
const txId = submitResponse.transactionId || this.resolvePaymentId(request);
|
|
3540
|
-
this.updateState({
|
|
3541
|
-
isLoading: false,
|
|
3542
|
-
transactionId: txId,
|
|
3543
|
-
currentScreen: LokotroPayScreenNavigation.MobileMoneyProcessingScreen
|
|
3544
|
-
});
|
|
3545
|
-
this.startMobileMoneyPolling(txId);
|
|
3546
|
-
return;
|
|
3547
|
-
}
|
|
3548
|
-
this.handlePaymentFailure(this.getString(responseData['message']) || response.message, response.apiResponseCode);
|
|
3549
|
-
}
|
|
3550
|
-
else {
|
|
3551
|
-
this.handlePaymentFailure(response.message, response.apiResponseCode);
|
|
3552
|
-
}
|
|
3553
|
-
}), catchError(error => {
|
|
3554
|
-
this.handlePaymentFailure(error.message);
|
|
3555
|
-
return throwError(() => error);
|
|
3556
|
-
}));
|
|
3497
|
+
parsePaymentInfo(data) {
|
|
3498
|
+
const additionalData = this.asRecord(data['additional_data']);
|
|
3499
|
+
// Drop feature-flag-hidden methods (e.g. Google/Apple Pay) so they never
|
|
3500
|
+
// enter the payment info model, get rendered, or get auto-selected.
|
|
3501
|
+
const availablePaymentMethods = visiblePaymentMethods(this.getRecordArray(data['available_payment_methods'] || additionalData?.['payment_methods'])
|
|
3502
|
+
.map(method => this.parsePaymentMethod(method)));
|
|
3503
|
+
const action = this.getString(additionalData?.['action']);
|
|
3504
|
+
return {
|
|
3505
|
+
id: this.resolvePaymentId(data),
|
|
3506
|
+
amount: this.getNumber(data['amount']),
|
|
3507
|
+
currency: this.getString(data['currency']) || 'USD',
|
|
3508
|
+
description: this.getString(data['description']),
|
|
3509
|
+
merchantName: this.getString(data['merchant_name']),
|
|
3510
|
+
merchantId: this.getString(data['merchant_id']),
|
|
3511
|
+
availablePaymentMethods,
|
|
3512
|
+
createdAt: new Date(this.getString(data['created_at']) || Date.now()),
|
|
3513
|
+
expiresAt: this.getString(data['expires_at']) ? new Date(this.getString(data['expires_at'])) : undefined,
|
|
3514
|
+
metadata: this.asRecord(data['metadata']),
|
|
3515
|
+
paymentUrl: this.getString(data['payment_url']) || undefined,
|
|
3516
|
+
showUserInfoForm: this.getBoolean(data['show_user_info_form']) || action === 'select_payment_method',
|
|
3517
|
+
showPaymentMethodForm: this.getBoolean(data['show_payment_method_form']) || action === 'select_payment_method'
|
|
3518
|
+
};
|
|
3557
3519
|
}
|
|
3558
3520
|
/**
|
|
3559
|
-
*
|
|
3560
|
-
* POST /payments/verify-otp
|
|
3521
|
+
* Parse payment info from transaction details response.
|
|
3561
3522
|
*/
|
|
3562
|
-
|
|
3563
|
-
this.
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
error: {
|
|
3582
|
-
message: response.message || 'OTP verification failed',
|
|
3583
|
-
title: 'Verification Failed',
|
|
3584
|
-
errorCode: response.apiResponseCode,
|
|
3585
|
-
timestamp: new Date()
|
|
3586
|
-
},
|
|
3587
|
-
currentScreen: LokotroPayScreenNavigation.ErrorScreen
|
|
3588
|
-
});
|
|
3589
|
-
}
|
|
3590
|
-
}), catchError(error => {
|
|
3591
|
-
this.updateState({
|
|
3592
|
-
isLoading: false,
|
|
3593
|
-
error: {
|
|
3594
|
-
message: error.message || 'OTP verification failed',
|
|
3595
|
-
title: 'Error',
|
|
3596
|
-
timestamp: new Date()
|
|
3597
|
-
},
|
|
3598
|
-
currentScreen: LokotroPayScreenNavigation.ErrorScreen
|
|
3599
|
-
});
|
|
3600
|
-
return throwError(() => error);
|
|
3601
|
-
}));
|
|
3523
|
+
parseTransactionPaymentInfo(data) {
|
|
3524
|
+
const fillingInfo = this.getString(data['filling_info']) || 'none';
|
|
3525
|
+
const channelInfo = this.getString(data['channel_info']) || 'none';
|
|
3526
|
+
return {
|
|
3527
|
+
id: this.resolvePaymentId(data),
|
|
3528
|
+
amount: this.getNumber(data['transactional_amount'] ?? data['amount']),
|
|
3529
|
+
currency: this.getString(data['transactional_currency']) || this.getString(data['currency_str']) || 'USD',
|
|
3530
|
+
description: this.getString(data['description']),
|
|
3531
|
+
merchantName: this.getString(data['merchant_name']),
|
|
3532
|
+
merchantId: this.getString(data['merchant_id']),
|
|
3533
|
+
availablePaymentMethods: visiblePaymentMethods(this.getRecordArray(data['payment_methods']).map(method => this.parsePaymentMethod(method))),
|
|
3534
|
+
createdAt: new Date(this.getString(data['created_at']) || Date.now()),
|
|
3535
|
+
metadata: this.asRecord(data['metadata']),
|
|
3536
|
+
paymentUrl: this.getString(data['payment_url']) || undefined,
|
|
3537
|
+
showUserInfoForm: fillingInfo === 'none',
|
|
3538
|
+
showPaymentMethodForm: channelInfo === 'none',
|
|
3539
|
+
fillingInfo,
|
|
3540
|
+
channelInfo
|
|
3541
|
+
};
|
|
3602
3542
|
}
|
|
3603
3543
|
/**
|
|
3604
|
-
*
|
|
3605
|
-
* POST /payments/resend-otp
|
|
3544
|
+
* Parse payment method from API response.
|
|
3606
3545
|
*/
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3546
|
+
parsePaymentMethod(data) {
|
|
3547
|
+
const nestedChannel = this.asRecord(data['channel']);
|
|
3548
|
+
const rawChannel = this.getString(nestedChannel?.['name']) ||
|
|
3549
|
+
this.getString(data['group']) ||
|
|
3550
|
+
this.getString(data['method']) ||
|
|
3551
|
+
this.getString(data['channel']) ||
|
|
3552
|
+
this.getString(data['name']);
|
|
3553
|
+
const name = this.getString(data['name']) || this.getString(data['method']) || rawChannel;
|
|
3554
|
+
return {
|
|
3555
|
+
id: this.getString(data['id']) || this.getString(data['payment_method_id']) || name,
|
|
3556
|
+
name,
|
|
3557
|
+
displayName: this.getString(data['display_name']) || name,
|
|
3558
|
+
channel: this.parseChannel(rawChannel),
|
|
3559
|
+
// PR-3.0 — pull the provider flag through. Legacy responses omit it →
|
|
3560
|
+
// remains undefined, consumers fall back to channel-level branching.
|
|
3561
|
+
flag: this.parsePaymentMethodFlag(this.getString(data['flag'])),
|
|
3562
|
+
iconUrl: this.getString(data['icon_url']) || this.getString(data['icon']),
|
|
3563
|
+
isEnabled: this.getBoolean(data['is_enabled'], this.getBoolean(data['available'], true)),
|
|
3564
|
+
configuration: this.asRecord(data['configuration']),
|
|
3565
|
+
supportedCurrencies: Array.isArray(data['supported_currencies'])
|
|
3566
|
+
? data['supported_currencies']
|
|
3567
|
+
: undefined
|
|
3568
|
+
};
|
|
3611
3569
|
}
|
|
3612
3570
|
/**
|
|
3613
|
-
*
|
|
3571
|
+
* PR-3.0 — narrow an arbitrary string to the LokotroPaymentMethodFlag union.
|
|
3572
|
+
* Unknown / empty inputs return undefined so the field stays optional.
|
|
3614
3573
|
*/
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3574
|
+
parsePaymentMethodFlag(value) {
|
|
3575
|
+
const known = [
|
|
3576
|
+
'none', 'all',
|
|
3577
|
+
'onafriq_mobile_money', 'onafriq_credit_card', 'rawbank_credit_card',
|
|
3578
|
+
'lokotro_wallet', 'lokotro_eflash',
|
|
3579
|
+
'cash', 'bank_transfer', 'google_pay', 'apple_pay',
|
|
3580
|
+
];
|
|
3581
|
+
return known.find(f => f === value);
|
|
3620
3582
|
}
|
|
3621
3583
|
/**
|
|
3622
|
-
*
|
|
3584
|
+
* Parse submit response from API.
|
|
3623
3585
|
*/
|
|
3624
|
-
|
|
3625
|
-
this.
|
|
3586
|
+
parseSubmitResponse(data) {
|
|
3587
|
+
const rawStatus = this.getString(data['status']);
|
|
3588
|
+
return {
|
|
3589
|
+
success: this.getBoolean(data['success']),
|
|
3590
|
+
message: this.getString(data['message']),
|
|
3591
|
+
transactionId: this.resolvePaymentId(data),
|
|
3592
|
+
status: LokotroPaymentStatusInfo.fromString(rawStatus),
|
|
3593
|
+
requiresOtp: this.getBoolean(data['requires_otp']) || rawStatus === 'pending_otp' || rawStatus === 'pending_otp_verification',
|
|
3594
|
+
otpDestination: this.getString(data['otp_destination']) || this.getString(data['otp_sent_to']) || undefined,
|
|
3595
|
+
redirectUrl: this.getString(data['redirect_url']) || this.getString(data['mastercardUrl']) || undefined
|
|
3596
|
+
};
|
|
3626
3597
|
}
|
|
3627
3598
|
/**
|
|
3628
|
-
*
|
|
3599
|
+
* Build an auto-submit request using the original payment body.
|
|
3629
3600
|
*/
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
this.stopMobileMoneyPolling();
|
|
3651
|
-
this.handlePaymentSuccess(data);
|
|
3652
|
-
}
|
|
3653
|
-
else if (status === 'failed' || status === 'error' || status === 'cancelled' || status === 'declined') {
|
|
3654
|
-
this.stopMobileMoneyPolling();
|
|
3655
|
-
this.handlePaymentFailure(this.getString(data['message']) || 'Payment failed or was declined');
|
|
3656
|
-
}
|
|
3657
|
-
},
|
|
3658
|
-
error: (_err) => {
|
|
3659
|
-
// PR-1.2: don't log full error object — may contain backend
|
|
3660
|
-
// response details that leak server internals to console.
|
|
3661
|
-
}
|
|
3662
|
-
});
|
|
3663
|
-
}, LokotroPaymentService.MOBILE_MONEY_POLL_INTERVAL);
|
|
3601
|
+
buildAutoSubmitRequest(paymentId, paymentMethodId, paymentBody) {
|
|
3602
|
+
return {
|
|
3603
|
+
paymentId,
|
|
3604
|
+
paymentMethodId,
|
|
3605
|
+
paymentMethod: paymentBody.paymentMethod,
|
|
3606
|
+
firstName: paymentBody.firstName,
|
|
3607
|
+
lastName: paymentBody.lastName,
|
|
3608
|
+
email: paymentBody.email,
|
|
3609
|
+
phoneNumber: paymentBody.phoneNumber,
|
|
3610
|
+
walletNumber: paymentBody.walletNumber,
|
|
3611
|
+
walletPin: paymentBody.walletPin,
|
|
3612
|
+
mobileMoneyPhoneNumber: paymentBody.mobileMoneyPhoneNumber,
|
|
3613
|
+
flashNumber: paymentBody.flashNumber,
|
|
3614
|
+
flashPin: paymentBody.flashPin,
|
|
3615
|
+
cardNumber: paymentBody.cardNumber,
|
|
3616
|
+
cardExpiryDate: paymentBody.cardExpiryDate,
|
|
3617
|
+
cardCvv: paymentBody.cardCvv,
|
|
3618
|
+
cardHolderName: paymentBody.cardHolderName,
|
|
3619
|
+
mastercardPaymentMethod: paymentBody.mastercardPaymentMethod
|
|
3620
|
+
};
|
|
3664
3621
|
}
|
|
3665
3622
|
/**
|
|
3666
|
-
*
|
|
3623
|
+
* Resolve the selected payment method from transaction details.
|
|
3667
3624
|
*/
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3625
|
+
resolveSelectedPaymentMethod(transactionData, paymentInfo) {
|
|
3626
|
+
const paymentMethodId = this.getString(transactionData['payment_method_id']);
|
|
3627
|
+
const paymentMethodName = this.getString(transactionData['payment_method_str']) || this.getString(transactionData['payment_method']);
|
|
3628
|
+
const normalizedName = this.normalizeMethodKey(paymentMethodName);
|
|
3629
|
+
const exactMatch = paymentInfo.availablePaymentMethods.find(method => method.id === paymentMethodId ||
|
|
3630
|
+
this.normalizeMethodKey(method.name) === normalizedName ||
|
|
3631
|
+
this.normalizeMethodKey(method.displayName) === normalizedName);
|
|
3632
|
+
if (exactMatch) {
|
|
3633
|
+
return exactMatch;
|
|
3634
|
+
}
|
|
3635
|
+
const channel = this.parseChannel(this.getString(transactionData['channel']) || paymentMethodName);
|
|
3636
|
+
if (!paymentMethodId && !paymentMethodName && channel === LokotroPayChannel.None) {
|
|
3637
|
+
return paymentInfo.availablePaymentMethods[0];
|
|
3673
3638
|
}
|
|
3639
|
+
return {
|
|
3640
|
+
id: paymentMethodId || paymentMethodName || channel,
|
|
3641
|
+
name: paymentMethodName || paymentMethodId || channel,
|
|
3642
|
+
displayName: paymentMethodName || paymentMethodId || channel,
|
|
3643
|
+
channel,
|
|
3644
|
+
iconUrl: '',
|
|
3645
|
+
isEnabled: true
|
|
3646
|
+
};
|
|
3674
3647
|
}
|
|
3675
3648
|
/**
|
|
3676
|
-
*
|
|
3677
|
-
* it. Defense-in-depth for Angular HIGH-1 — server-side per-merchant
|
|
3678
|
-
* allow-list (PR-2.0) is the real fix.
|
|
3679
|
-
*
|
|
3680
|
-
* Policy:
|
|
3681
|
-
* - Reject non-http(s) schemes (blocks `javascript:`, `data:`, `file:`,
|
|
3682
|
-
* `intent:`, custom schemes).
|
|
3683
|
-
* - Reject http:// in production.
|
|
3684
|
-
* - Reject malformed URLs.
|
|
3685
|
-
* - SSR safety: skip the redirect when window is unavailable.
|
|
3649
|
+
* Normalize payloads that sometimes return data nested inside data.
|
|
3686
3650
|
*/
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
if (!url || typeof url !== 'string') {
|
|
3692
|
-
this.handlePaymentFailure('Invalid redirect URL');
|
|
3693
|
-
return;
|
|
3651
|
+
unwrapPayload(payload) {
|
|
3652
|
+
const record = this.asRecord(payload);
|
|
3653
|
+
if (!record) {
|
|
3654
|
+
return {};
|
|
3694
3655
|
}
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3656
|
+
const nestedData = this.asRecord(record['data']);
|
|
3657
|
+
return nestedData || record;
|
|
3658
|
+
}
|
|
3659
|
+
resolvePaymentId(source) {
|
|
3660
|
+
return this.getString(source['payment_id']) ||
|
|
3661
|
+
this.getString(source['transaction_id']) ||
|
|
3662
|
+
this.getString(source['paymentId']) ||
|
|
3663
|
+
this.getString(source['transactionId']) ||
|
|
3664
|
+
this.currentState.transactionId ||
|
|
3665
|
+
'';
|
|
3666
|
+
}
|
|
3667
|
+
parseChannel(channel) {
|
|
3668
|
+
switch (this.normalizeMethodKey(channel)) {
|
|
3669
|
+
case 'card':
|
|
3670
|
+
return LokotroPayChannel.Card;
|
|
3671
|
+
case 'mobilemoney':
|
|
3672
|
+
return LokotroPayChannel.MobileMoney;
|
|
3673
|
+
case 'wallet':
|
|
3674
|
+
case 'ewallet':
|
|
3675
|
+
case 'lokotrowallet':
|
|
3676
|
+
return LokotroPayChannel.EWallet;
|
|
3677
|
+
case 'flash':
|
|
3678
|
+
case 'eflash':
|
|
3679
|
+
return LokotroPayChannel.EFlash;
|
|
3680
|
+
case 'banktransfer':
|
|
3681
|
+
return LokotroPayChannel.BankTransfer;
|
|
3682
|
+
default:
|
|
3683
|
+
return LokotroPayChannel.None;
|
|
3698
3684
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3685
|
+
}
|
|
3686
|
+
normalizeMethodKey(value) {
|
|
3687
|
+
return value.toLowerCase().replace(/[_\s-]/g, '');
|
|
3688
|
+
}
|
|
3689
|
+
getNumber(value, fallback = 0) {
|
|
3690
|
+
if (typeof value === 'number') {
|
|
3691
|
+
return Number.isFinite(value) ? value : fallback;
|
|
3702
3692
|
}
|
|
3703
|
-
if (
|
|
3704
|
-
|
|
3705
|
-
return;
|
|
3693
|
+
if (typeof value === 'string') {
|
|
3694
|
+
const parsedValue = parseFloat(value);
|
|
3695
|
+
return Number.isFinite(parsedValue) ? parsedValue : fallback;
|
|
3706
3696
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3697
|
+
return fallback;
|
|
3698
|
+
}
|
|
3699
|
+
getString(value) {
|
|
3700
|
+
return typeof value === 'string' ? value : '';
|
|
3701
|
+
}
|
|
3702
|
+
getBoolean(value, fallback = false) {
|
|
3703
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
3704
|
+
}
|
|
3705
|
+
asRecord(value) {
|
|
3706
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
3707
|
+
? value
|
|
3708
|
+
: undefined;
|
|
3709
|
+
}
|
|
3710
|
+
getRecordArray(value) {
|
|
3711
|
+
if (!Array.isArray(value)) {
|
|
3712
|
+
return [];
|
|
3711
3713
|
}
|
|
3712
|
-
|
|
3714
|
+
return value
|
|
3715
|
+
.map(item => this.asRecord(item))
|
|
3716
|
+
.filter((item) => Boolean(item));
|
|
3717
|
+
}
|
|
3718
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroPaymentService, deps: [{ token: LokotroHttpClientService }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3719
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroPaymentService, providedIn: 'root' }); }
|
|
3720
|
+
}
|
|
3721
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroPaymentService, decorators: [{
|
|
3722
|
+
type: Injectable,
|
|
3723
|
+
args: [{
|
|
3724
|
+
providedIn: 'root'
|
|
3725
|
+
}]
|
|
3726
|
+
}], ctorParameters: () => [{ type: LokotroHttpClientService }] });
|
|
3727
|
+
|
|
3728
|
+
/**
|
|
3729
|
+
* Lokotro Pay - Bank Transfer Form Component
|
|
3730
|
+
*/
|
|
3731
|
+
class LokotroBankTransferFormComponent {
|
|
3732
|
+
constructor(fb, localization, paymentService) {
|
|
3733
|
+
this.fb = fb;
|
|
3734
|
+
this.localization = localization;
|
|
3735
|
+
this.paymentService = paymentService;
|
|
3736
|
+
this.showUserInfoForm = false;
|
|
3737
|
+
this.formSubmitted = new EventEmitter();
|
|
3738
|
+
this.cancel = new EventEmitter();
|
|
3739
|
+
this.isLoading = true;
|
|
3740
|
+
this.allBanks = [];
|
|
3741
|
+
this.cities = [];
|
|
3742
|
+
this.filteredBanks = [];
|
|
3743
|
+
this.accounts = [];
|
|
3744
|
+
this.selectedCity = null;
|
|
3745
|
+
this.selectedBank = null;
|
|
3746
|
+
this.selectedAccount = null;
|
|
3713
3747
|
}
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
const
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
customRef: this.getString(data['custom_ref']) || this.getString(data['customer_reference']),
|
|
3724
|
-
amount: this.getNumber(data['amount']),
|
|
3725
|
-
apiResponseCode: LokotroPayApiResponseCode.LOK000,
|
|
3726
|
-
currency: this.getString(data['currency']) || this.getString(data['currency_str']),
|
|
3727
|
-
paymentStatus,
|
|
3728
|
-
systemRef: this.getString(data['system_reference']) || this.getString(data['system_ref']) || transactionId,
|
|
3729
|
-
transactionId,
|
|
3730
|
-
timestamp: new Date()
|
|
3748
|
+
ngOnInit() {
|
|
3749
|
+
this.initForm();
|
|
3750
|
+
this.fetchBanks();
|
|
3751
|
+
}
|
|
3752
|
+
initForm() {
|
|
3753
|
+
const config = {
|
|
3754
|
+
city: ['', Validators.required],
|
|
3755
|
+
bank: [{ value: '', disabled: true }, Validators.required],
|
|
3756
|
+
account: [{ value: '', disabled: true }, Validators.required]
|
|
3731
3757
|
};
|
|
3732
|
-
this.
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3758
|
+
if (this.showUserInfoForm) {
|
|
3759
|
+
config['firstName'] = ['', Validators.required];
|
|
3760
|
+
config['lastName'] = ['', Validators.required];
|
|
3761
|
+
config['email'] = ['', [Validators.required, Validators.email]];
|
|
3762
|
+
config['personalPhone'] = ['', Validators.required];
|
|
3763
|
+
}
|
|
3764
|
+
this.bankTransferForm = this.fb.group(config);
|
|
3737
3765
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
title: 'Payment Failed',
|
|
3748
|
-
errorCode,
|
|
3749
|
-
timestamp: new Date()
|
|
3766
|
+
fetchBanks() {
|
|
3767
|
+
this.isLoading = true;
|
|
3768
|
+
this.paymentService.fetchAvailableBanks().subscribe({
|
|
3769
|
+
next: (response) => {
|
|
3770
|
+
if (response.isSuccess && response.data) {
|
|
3771
|
+
this.allBanks = response.data;
|
|
3772
|
+
this.extractCities();
|
|
3773
|
+
}
|
|
3774
|
+
this.isLoading = false;
|
|
3750
3775
|
},
|
|
3751
|
-
|
|
3776
|
+
error: (err) => {
|
|
3777
|
+
console.error('Failed to fetch banks', err);
|
|
3778
|
+
this.isLoading = false;
|
|
3779
|
+
}
|
|
3752
3780
|
});
|
|
3753
3781
|
}
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
const additionalData = this.asRecord(collectData['additional_data']);
|
|
3762
|
-
if (paymentStatus === 'pending_otp' || paymentStatus === 'pending_otp_verification') {
|
|
3763
|
-
this.updateState({
|
|
3764
|
-
isLoading: false,
|
|
3765
|
-
transactionId: paymentId,
|
|
3766
|
-
paymentInfo,
|
|
3767
|
-
currentScreen: LokotroPayScreenNavigation.EWalletOtpScreen
|
|
3782
|
+
extractCities() {
|
|
3783
|
+
const cityMap = new Map();
|
|
3784
|
+
this.allBanks.forEach(bank => {
|
|
3785
|
+
bank.entityAvailables.forEach(city => {
|
|
3786
|
+
if (!cityMap.has(city.id)) {
|
|
3787
|
+
cityMap.set(city.id, city);
|
|
3788
|
+
}
|
|
3768
3789
|
});
|
|
3769
|
-
|
|
3790
|
+
});
|
|
3791
|
+
this.cities = Array.from(cityMap.values());
|
|
3792
|
+
}
|
|
3793
|
+
onCityChange() {
|
|
3794
|
+
const cityId = this.bankTransferForm.get('city')?.value;
|
|
3795
|
+
const bankControl = this.bankTransferForm.get('bank');
|
|
3796
|
+
const accountControl = this.bankTransferForm.get('account');
|
|
3797
|
+
bankControl?.reset('');
|
|
3798
|
+
bankControl?.disable();
|
|
3799
|
+
accountControl?.reset('');
|
|
3800
|
+
accountControl?.disable();
|
|
3801
|
+
this.filteredBanks = [];
|
|
3802
|
+
this.accounts = [];
|
|
3803
|
+
this.selectedAccount = null;
|
|
3804
|
+
if (cityId) {
|
|
3805
|
+
this.selectedCity = this.cities.find(c => c.id === cityId) || null;
|
|
3806
|
+
this.filteredBanks = this.allBanks.filter(bank => bank.entityAvailables.some(e => e.id === cityId));
|
|
3807
|
+
bankControl?.enable();
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
onBankChange() {
|
|
3811
|
+
const bankId = this.bankTransferForm.get('bank')?.value;
|
|
3812
|
+
const accountControl = this.bankTransferForm.get('account');
|
|
3813
|
+
accountControl?.reset('');
|
|
3814
|
+
accountControl?.disable();
|
|
3815
|
+
this.accounts = [];
|
|
3816
|
+
this.selectedAccount = null;
|
|
3817
|
+
if (bankId) {
|
|
3818
|
+
this.selectedBank = this.filteredBanks.find(b => b.id === bankId) || null;
|
|
3819
|
+
if (this.selectedBank) {
|
|
3820
|
+
this.accounts = this.selectedBank.bankAccounts;
|
|
3821
|
+
accountControl?.enable();
|
|
3822
|
+
}
|
|
3770
3823
|
}
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3824
|
+
}
|
|
3825
|
+
onAccountChange() {
|
|
3826
|
+
const accountId = this.bankTransferForm.get('account')?.value;
|
|
3827
|
+
if (accountId) {
|
|
3828
|
+
this.selectedAccount = this.accounts.find(a => a.id === accountId) || null;
|
|
3774
3829
|
}
|
|
3775
|
-
|
|
3776
|
-
this.
|
|
3777
|
-
return of(void 0);
|
|
3830
|
+
else {
|
|
3831
|
+
this.selectedAccount = null;
|
|
3778
3832
|
}
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3833
|
+
}
|
|
3834
|
+
onSubmit() {
|
|
3835
|
+
if (this.bankTransferForm.valid && this.selectedAccount) {
|
|
3836
|
+
const submitData = {
|
|
3837
|
+
bankTransferAccountId: this.selectedAccount.id
|
|
3838
|
+
};
|
|
3839
|
+
if (this.showUserInfoForm) {
|
|
3840
|
+
submitData['firstName'] = this.bankTransferForm.value.firstName;
|
|
3841
|
+
submitData['lastName'] = this.bankTransferForm.value.lastName;
|
|
3842
|
+
submitData['email'] = this.bankTransferForm.value.email;
|
|
3843
|
+
submitData['phoneNumber'] = this.bankTransferForm.value.personalPhone;
|
|
3786
3844
|
}
|
|
3787
|
-
this.
|
|
3788
|
-
isLoading: false,
|
|
3789
|
-
transactionId: paymentId
|
|
3790
|
-
});
|
|
3791
|
-
// PR-1.2 / Angular HIGH-1: validate before redirect.
|
|
3792
|
-
this.redirectIfAllowed(redirectUrl, 'session-created');
|
|
3793
|
-
return of(void 0);
|
|
3845
|
+
this.formSubmitted.emit(submitData);
|
|
3794
3846
|
}
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3847
|
+
}
|
|
3848
|
+
onCancel() {
|
|
3849
|
+
this.cancel.emit();
|
|
3850
|
+
}
|
|
3851
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroBankTransferFormComponent, deps: [{ token: i1$1.FormBuilder }, { token: LokotroLocalizationService }, { token: LokotroPaymentService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3852
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: LokotroBankTransferFormComponent, isStandalone: true, selector: "lokotro-bank-transfer-form", inputs: { showUserInfoForm: "showUserInfoForm" }, outputs: { formSubmitted: "formSubmitted", cancel: "cancel" }, ngImport: i0, template: `
|
|
3853
|
+
<div class="lokotro-bank-transfer-form">
|
|
3854
|
+
<form [formGroup]="bankTransferForm" (ngSubmit)="onSubmit()">
|
|
3855
|
+
<h3 class="lokotro-form-title">{{ localization.translate('bankTransfer') || 'Bank Transfer' }}</h3>
|
|
3856
|
+
|
|
3857
|
+
<!-- Personal Information Section -->
|
|
3858
|
+
@if (showUserInfoForm) {
|
|
3859
|
+
<div class="lokotro-user-info-section">
|
|
3860
|
+
<h4 class="lokotro-section-subtitle">{{ localization.translate('personalInfo') }}</h4>
|
|
3861
|
+
<div class="lokotro-form-row">
|
|
3862
|
+
<div class="lokotro-form-group">
|
|
3863
|
+
<label class="lokotro-label">{{ localization.translate('firstName') }}</label>
|
|
3864
|
+
<input type="text" class="lokotro-input" formControlName="firstName" placeholder="John">
|
|
3865
|
+
@if (bankTransferForm.get('firstName')?.touched && bankTransferForm.get('firstName')?.invalid) {
|
|
3866
|
+
<span class="lokotro-error">
|
|
3867
|
+
{{ localization.translate('required') }}
|
|
3868
|
+
</span>
|
|
3869
|
+
}
|
|
3870
|
+
</div>
|
|
3871
|
+
<div class="lokotro-form-group">
|
|
3872
|
+
<label class="lokotro-label">{{ localization.translate('lastName') }}</label>
|
|
3873
|
+
<input type="text" class="lokotro-input" formControlName="lastName" placeholder="Doe">
|
|
3874
|
+
@if (bankTransferForm.get('lastName')?.touched && bankTransferForm.get('lastName')?.invalid) {
|
|
3875
|
+
<span class="lokotro-error">
|
|
3876
|
+
{{ localization.translate('required') }}
|
|
3877
|
+
</span>
|
|
3878
|
+
}
|
|
3879
|
+
</div>
|
|
3880
|
+
</div>
|
|
3881
|
+
<div class="lokotro-form-group">
|
|
3882
|
+
<label class="lokotro-label">{{ localization.translate('email') }}</label>
|
|
3883
|
+
<input type="email" class="lokotro-input" formControlName="email" placeholder="john.doe@example.com">
|
|
3884
|
+
@if (bankTransferForm.get('email')?.touched && bankTransferForm.get('email')?.invalid) {
|
|
3885
|
+
<span class="lokotro-error">
|
|
3886
|
+
{{ localization.translate('invalidEmail') }}
|
|
3887
|
+
</span>
|
|
3888
|
+
}
|
|
3889
|
+
</div>
|
|
3890
|
+
<div class="lokotro-form-group">
|
|
3891
|
+
<label class="lokotro-label">{{ localization.translate('phoneNumber') }}</label>
|
|
3892
|
+
<input type="tel" class="lokotro-input" formControlName="personalPhone" placeholder="+243 XXX XXX XXX">
|
|
3893
|
+
@if (bankTransferForm.get('personalPhone')?.touched && bankTransferForm.get('personalPhone')?.invalid) {
|
|
3894
|
+
<span class="lokotro-error">
|
|
3895
|
+
{{ localization.translate('invalidPhoneNumber') }}
|
|
3896
|
+
</span>
|
|
3897
|
+
}
|
|
3898
|
+
</div>
|
|
3899
|
+
<hr class="lokotro-divider">
|
|
3900
|
+
</div>
|
|
3803
3901
|
}
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3902
|
+
|
|
3903
|
+
<!-- Bank Selection Section -->
|
|
3904
|
+
@if (showUserInfoForm) {
|
|
3905
|
+
<h4 class="lokotro-section-subtitle">{{ localization.translate('bankDetails') || 'Bank Details' }}</h4>
|
|
3807
3906
|
}
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3907
|
+
|
|
3908
|
+
<!-- Loading State -->
|
|
3909
|
+
@if (isLoading) {
|
|
3910
|
+
<div class="lokotro-loading-text">
|
|
3911
|
+
{{ localization.translate('loading') || 'Loading...' }}
|
|
3912
|
+
</div>
|
|
3811
3913
|
}
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3914
|
+
|
|
3915
|
+
@if (!isLoading) {
|
|
3916
|
+
<div>
|
|
3917
|
+
<!-- City Selection -->
|
|
3918
|
+
<div class="lokotro-form-group">
|
|
3919
|
+
<label class="lokotro-label">{{ localization.translate('selectCity') || 'Select City' }}</label>
|
|
3920
|
+
<select class="lokotro-input" formControlName="city" (change)="onCityChange()">
|
|
3921
|
+
<option value="">{{ localization.translate('selectCity') || 'Select City' }}</option>
|
|
3922
|
+
@for (city of cities; track city) {
|
|
3923
|
+
<option [value]="city.id">{{ city.name }}</option>
|
|
3924
|
+
}
|
|
3925
|
+
</select>
|
|
3926
|
+
</div>
|
|
3927
|
+
<!-- Bank Selection -->
|
|
3928
|
+
<div class="lokotro-form-group">
|
|
3929
|
+
<label class="lokotro-label">{{ localization.translate('selectBank') || 'Select Bank' }}</label>
|
|
3930
|
+
<select class="lokotro-input" formControlName="bank" (change)="onBankChange()">
|
|
3931
|
+
<option value="">{{ localization.translate('selectBank') || 'Select Bank' }}</option>
|
|
3932
|
+
@for (bank of filteredBanks; track bank) {
|
|
3933
|
+
<option [value]="bank.id">{{ bank.name }}</option>
|
|
3934
|
+
}
|
|
3935
|
+
</select>
|
|
3936
|
+
</div>
|
|
3937
|
+
<!-- Account Selection -->
|
|
3938
|
+
<div class="lokotro-form-group">
|
|
3939
|
+
<label class="lokotro-label">{{ localization.translate('selectBankAccount') || 'Select Account' }}</label>
|
|
3940
|
+
<select class="lokotro-input" formControlName="account" (change)="onAccountChange()">
|
|
3941
|
+
<option value="">{{ localization.translate('selectBankAccount') || 'Select Account' }}</option>
|
|
3942
|
+
@for (account of accounts; track account) {
|
|
3943
|
+
<option [value]="account.id">{{ account.accountLabel }} ({{ account.accountNumber }})</option>
|
|
3944
|
+
}
|
|
3945
|
+
</select>
|
|
3946
|
+
</div>
|
|
3947
|
+
</div>
|
|
3821
3948
|
}
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
if (
|
|
3825
|
-
|
|
3826
|
-
|
|
3949
|
+
|
|
3950
|
+
<!-- Selected Account Summary -->
|
|
3951
|
+
@if (selectedAccount) {
|
|
3952
|
+
<div class="lokotro-account-summary">
|
|
3953
|
+
<div class="lokotro-info-box">
|
|
3954
|
+
<p><strong>{{ localization.translate('bankAccountSummary') || 'Account Number' }}:</strong> {{ selectedAccount.accountNumber }}</p>
|
|
3955
|
+
<p><strong>{{ localization.translate('accountLabel') || 'Label' }}:</strong> {{ selectedAccount.accountLabel }}</p>
|
|
3956
|
+
<p class="lokotro-info-text">
|
|
3957
|
+
{{ localization.translate('bankTransferProofInstructions') || 'A payment link will be sent to your email to upload the proof of transfer.' }}
|
|
3958
|
+
</p>
|
|
3959
|
+
</div>
|
|
3960
|
+
</div>
|
|
3827
3961
|
}
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3962
|
+
|
|
3963
|
+
<div class="lokotro-form-actions">
|
|
3964
|
+
<button type="button" class="lokotro-btn-secondary" (click)="onCancel()">
|
|
3965
|
+
{{ localization.translate('cancel') }}
|
|
3966
|
+
</button>
|
|
3967
|
+
<button type="submit" class="lokotro-btn-primary" [disabled]="bankTransferForm.invalid || isLoading">
|
|
3968
|
+
{{ localization.translate('confirmBankTransfer') || 'Confirm Transfer' }}
|
|
3969
|
+
</button>
|
|
3970
|
+
</div>
|
|
3971
|
+
</form>
|
|
3972
|
+
</div>
|
|
3973
|
+
`, isInline: true, styles: [".lokotro-bank-transfer-form{width:100%}.lokotro-form-title{font-size:20px;font-weight:600;margin:0 0 24px;color:var(--lokotro-text-primary, #F2F0D5);text-align:center}.lokotro-form-group{margin-bottom:20px}.lokotro-form-row{display:flex;gap:16px}.lokotro-form-row .lokotro-form-group{flex:1}.lokotro-label{display:block;margin-bottom:8px;font-size:14px;font-weight:500;color:var(--lokotro-text-secondary, #D5D3B8)}.lokotro-input{width:100%;padding:14px 16px;background:var(--lokotro-card, #3A4840);border:2px solid var(--lokotro-border, #3A473F);border-radius:12px;color:var(--lokotro-text-primary, #F2F0D5);font-size:16px;transition:border-color .2s;box-sizing:border-box}.lokotro-input:focus{outline:none;border-color:var(--lokotro-accent, #3BFBDA)}.lokotro-error{display:block;margin-top:6px;font-size:12px;color:var(--lokotro-error, #D97652)}.lokotro-user-info-section{margin-bottom:24px}.lokotro-section-subtitle{font-size:16px;font-weight:600;margin:0 0 16px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-divider{border:none;border-top:1px solid var(--lokotro-border, #3A473F);margin:24px 0}.lokotro-form-actions{display:flex;gap:12px;margin-top:32px}.lokotro-btn-primary,.lokotro-btn-secondary{flex:1;padding:14px 24px;border-radius:12px;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s ease}.lokotro-btn-primary{background:linear-gradient(135deg,var(--lokotro-primary, #5A5E39),var(--lokotro-secondary, #6E7346));color:var(--lokotro-text-primary, #F2F0D5);border:none}.lokotro-btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 16px #0003}.lokotro-btn-primary:disabled{opacity:.5;cursor:not-allowed}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #F2F0D5);border:2px solid var(--lokotro-border, #3A473F)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #D5D3B8)}.lokotro-loading-text{text-align:center;color:var(--lokotro-text-secondary, #D5D3B8);padding:20px}.lokotro-account-summary{margin-top:20px;padding:16px;background:#3a484080;border-radius:12px;border:1px solid var(--lokotro-border, #3A473F)}.lokotro-info-box p{margin:8px 0;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-info-text{font-style:italic;color:var(--lokotro-text-secondary, #D5D3B8);font-size:14px;margin-top:12px!important}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }] }); }
|
|
3974
|
+
}
|
|
3975
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroBankTransferFormComponent, decorators: [{
|
|
3976
|
+
type: Component,
|
|
3977
|
+
args: [{ selector: 'lokotro-bank-transfer-form', standalone: true, imports: [FormsModule, ReactiveFormsModule], template: `
|
|
3978
|
+
<div class="lokotro-bank-transfer-form">
|
|
3979
|
+
<form [formGroup]="bankTransferForm" (ngSubmit)="onSubmit()">
|
|
3980
|
+
<h3 class="lokotro-form-title">{{ localization.translate('bankTransfer') || 'Bank Transfer' }}</h3>
|
|
3981
|
+
|
|
3982
|
+
<!-- Personal Information Section -->
|
|
3983
|
+
@if (showUserInfoForm) {
|
|
3984
|
+
<div class="lokotro-user-info-section">
|
|
3985
|
+
<h4 class="lokotro-section-subtitle">{{ localization.translate('personalInfo') }}</h4>
|
|
3986
|
+
<div class="lokotro-form-row">
|
|
3987
|
+
<div class="lokotro-form-group">
|
|
3988
|
+
<label class="lokotro-label">{{ localization.translate('firstName') }}</label>
|
|
3989
|
+
<input type="text" class="lokotro-input" formControlName="firstName" placeholder="John">
|
|
3990
|
+
@if (bankTransferForm.get('firstName')?.touched && bankTransferForm.get('firstName')?.invalid) {
|
|
3991
|
+
<span class="lokotro-error">
|
|
3992
|
+
{{ localization.translate('required') }}
|
|
3993
|
+
</span>
|
|
3844
3994
|
}
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3995
|
+
</div>
|
|
3996
|
+
<div class="lokotro-form-group">
|
|
3997
|
+
<label class="lokotro-label">{{ localization.translate('lastName') }}</label>
|
|
3998
|
+
<input type="text" class="lokotro-input" formControlName="lastName" placeholder="Doe">
|
|
3999
|
+
@if (bankTransferForm.get('lastName')?.touched && bankTransferForm.get('lastName')?.invalid) {
|
|
4000
|
+
<span class="lokotro-error">
|
|
4001
|
+
{{ localization.translate('required') }}
|
|
4002
|
+
</span>
|
|
4003
|
+
}
|
|
4004
|
+
</div>
|
|
4005
|
+
</div>
|
|
4006
|
+
<div class="lokotro-form-group">
|
|
4007
|
+
<label class="lokotro-label">{{ localization.translate('email') }}</label>
|
|
4008
|
+
<input type="email" class="lokotro-input" formControlName="email" placeholder="john.doe@example.com">
|
|
4009
|
+
@if (bankTransferForm.get('email')?.touched && bankTransferForm.get('email')?.invalid) {
|
|
4010
|
+
<span class="lokotro-error">
|
|
4011
|
+
{{ localization.translate('invalidEmail') }}
|
|
4012
|
+
</span>
|
|
4013
|
+
}
|
|
4014
|
+
</div>
|
|
4015
|
+
<div class="lokotro-form-group">
|
|
4016
|
+
<label class="lokotro-label">{{ localization.translate('phoneNumber') }}</label>
|
|
4017
|
+
<input type="tel" class="lokotro-input" formControlName="personalPhone" placeholder="+243 XXX XXX XXX">
|
|
4018
|
+
@if (bankTransferForm.get('personalPhone')?.touched && bankTransferForm.get('personalPhone')?.invalid) {
|
|
4019
|
+
<span class="lokotro-error">
|
|
4020
|
+
{{ localization.translate('invalidPhoneNumber') }}
|
|
4021
|
+
</span>
|
|
4022
|
+
}
|
|
4023
|
+
</div>
|
|
4024
|
+
<hr class="lokotro-divider">
|
|
4025
|
+
</div>
|
|
3852
4026
|
}
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
4027
|
+
|
|
4028
|
+
<!-- Bank Selection Section -->
|
|
4029
|
+
@if (showUserInfoForm) {
|
|
4030
|
+
<h4 class="lokotro-section-subtitle">{{ localization.translate('bankDetails') || 'Bank Details' }}</h4>
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
<!-- Loading State -->
|
|
4034
|
+
@if (isLoading) {
|
|
4035
|
+
<div class="lokotro-loading-text">
|
|
4036
|
+
{{ localization.translate('loading') || 'Loading...' }}
|
|
4037
|
+
</div>
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
@if (!isLoading) {
|
|
4041
|
+
<div>
|
|
4042
|
+
<!-- City Selection -->
|
|
4043
|
+
<div class="lokotro-form-group">
|
|
4044
|
+
<label class="lokotro-label">{{ localization.translate('selectCity') || 'Select City' }}</label>
|
|
4045
|
+
<select class="lokotro-input" formControlName="city" (change)="onCityChange()">
|
|
4046
|
+
<option value="">{{ localization.translate('selectCity') || 'Select City' }}</option>
|
|
4047
|
+
@for (city of cities; track city) {
|
|
4048
|
+
<option [value]="city.id">{{ city.name }}</option>
|
|
4049
|
+
}
|
|
4050
|
+
</select>
|
|
4051
|
+
</div>
|
|
4052
|
+
<!-- Bank Selection -->
|
|
4053
|
+
<div class="lokotro-form-group">
|
|
4054
|
+
<label class="lokotro-label">{{ localization.translate('selectBank') || 'Select Bank' }}</label>
|
|
4055
|
+
<select class="lokotro-input" formControlName="bank" (change)="onBankChange()">
|
|
4056
|
+
<option value="">{{ localization.translate('selectBank') || 'Select Bank' }}</option>
|
|
4057
|
+
@for (bank of filteredBanks; track bank) {
|
|
4058
|
+
<option [value]="bank.id">{{ bank.name }}</option>
|
|
4059
|
+
}
|
|
4060
|
+
</select>
|
|
4061
|
+
</div>
|
|
4062
|
+
<!-- Account Selection -->
|
|
4063
|
+
<div class="lokotro-form-group">
|
|
4064
|
+
<label class="lokotro-label">{{ localization.translate('selectBankAccount') || 'Select Account' }}</label>
|
|
4065
|
+
<select class="lokotro-input" formControlName="account" (change)="onAccountChange()">
|
|
4066
|
+
<option value="">{{ localization.translate('selectBankAccount') || 'Select Account' }}</option>
|
|
4067
|
+
@for (account of accounts; track account) {
|
|
4068
|
+
<option [value]="account.id">{{ account.accountLabel }} ({{ account.accountNumber }})</option>
|
|
4069
|
+
}
|
|
4070
|
+
</select>
|
|
4071
|
+
</div>
|
|
4072
|
+
</div>
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
<!-- Selected Account Summary -->
|
|
4076
|
+
@if (selectedAccount) {
|
|
4077
|
+
<div class="lokotro-account-summary">
|
|
4078
|
+
<div class="lokotro-info-box">
|
|
4079
|
+
<p><strong>{{ localization.translate('bankAccountSummary') || 'Account Number' }}:</strong> {{ selectedAccount.accountNumber }}</p>
|
|
4080
|
+
<p><strong>{{ localization.translate('accountLabel') || 'Label' }}:</strong> {{ selectedAccount.accountLabel }}</p>
|
|
4081
|
+
<p class="lokotro-info-text">
|
|
4082
|
+
{{ localization.translate('bankTransferProofInstructions') || 'A payment link will be sent to your email to upload the proof of transfer.' }}
|
|
4083
|
+
</p>
|
|
4084
|
+
</div>
|
|
4085
|
+
</div>
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
<div class="lokotro-form-actions">
|
|
4089
|
+
<button type="button" class="lokotro-btn-secondary" (click)="onCancel()">
|
|
4090
|
+
{{ localization.translate('cancel') }}
|
|
4091
|
+
</button>
|
|
4092
|
+
<button type="submit" class="lokotro-btn-primary" [disabled]="bankTransferForm.invalid || isLoading">
|
|
4093
|
+
{{ localization.translate('confirmBankTransfer') || 'Confirm Transfer' }}
|
|
4094
|
+
</button>
|
|
4095
|
+
</div>
|
|
4096
|
+
</form>
|
|
4097
|
+
</div>
|
|
4098
|
+
`, styles: [".lokotro-bank-transfer-form{width:100%}.lokotro-form-title{font-size:20px;font-weight:600;margin:0 0 24px;color:var(--lokotro-text-primary, #F2F0D5);text-align:center}.lokotro-form-group{margin-bottom:20px}.lokotro-form-row{display:flex;gap:16px}.lokotro-form-row .lokotro-form-group{flex:1}.lokotro-label{display:block;margin-bottom:8px;font-size:14px;font-weight:500;color:var(--lokotro-text-secondary, #D5D3B8)}.lokotro-input{width:100%;padding:14px 16px;background:var(--lokotro-card, #3A4840);border:2px solid var(--lokotro-border, #3A473F);border-radius:12px;color:var(--lokotro-text-primary, #F2F0D5);font-size:16px;transition:border-color .2s;box-sizing:border-box}.lokotro-input:focus{outline:none;border-color:var(--lokotro-accent, #3BFBDA)}.lokotro-error{display:block;margin-top:6px;font-size:12px;color:var(--lokotro-error, #D97652)}.lokotro-user-info-section{margin-bottom:24px}.lokotro-section-subtitle{font-size:16px;font-weight:600;margin:0 0 16px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-divider{border:none;border-top:1px solid var(--lokotro-border, #3A473F);margin:24px 0}.lokotro-form-actions{display:flex;gap:12px;margin-top:32px}.lokotro-btn-primary,.lokotro-btn-secondary{flex:1;padding:14px 24px;border-radius:12px;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s ease}.lokotro-btn-primary{background:linear-gradient(135deg,var(--lokotro-primary, #5A5E39),var(--lokotro-secondary, #6E7346));color:var(--lokotro-text-primary, #F2F0D5);border:none}.lokotro-btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 16px #0003}.lokotro-btn-primary:disabled{opacity:.5;cursor:not-allowed}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #F2F0D5);border:2px solid var(--lokotro-border, #3A473F)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #D5D3B8)}.lokotro-loading-text{text-align:center;color:var(--lokotro-text-secondary, #D5D3B8);padding:20px}.lokotro-account-summary{margin-top:20px;padding:16px;background:#3a484080;border-radius:12px;border:1px solid var(--lokotro-border, #3A473F)}.lokotro-info-box p{margin:8px 0;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-info-text{font-style:italic;color:var(--lokotro-text-secondary, #D5D3B8);font-size:14px;margin-top:12px!important}\n"] }]
|
|
4099
|
+
}], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: LokotroLocalizationService }, { type: LokotroPaymentService }], propDecorators: { showUserInfoForm: [{
|
|
4100
|
+
type: Input
|
|
4101
|
+
}], formSubmitted: [{
|
|
4102
|
+
type: Output
|
|
4103
|
+
}], cancel: [{
|
|
4104
|
+
type: Output
|
|
4105
|
+
}] } });
|
|
4106
|
+
|
|
4107
|
+
/**
|
|
4108
|
+
* Lokotro Pay - OTP Verification Component
|
|
4109
|
+
*/
|
|
4110
|
+
class LokotroOtpVerificationComponent {
|
|
4111
|
+
constructor(localization) {
|
|
4112
|
+
this.localization = localization;
|
|
4113
|
+
this.otpLength = 6;
|
|
4114
|
+
this.otpVerified = new EventEmitter();
|
|
4115
|
+
this.resendOtp = new EventEmitter();
|
|
4116
|
+
this.cancel = new EventEmitter();
|
|
4117
|
+
this.otpDigits = [];
|
|
4118
|
+
this.resendTimer = 60;
|
|
3864
4119
|
}
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
return LokotroPayScreenNavigation.EWalletFormScreen;
|
|
3873
|
-
case LokotroPayChannel.MobileMoney:
|
|
3874
|
-
return LokotroPayScreenNavigation.MobileMoneyFormScreen;
|
|
3875
|
-
case LokotroPayChannel.Card:
|
|
3876
|
-
case LokotroPayChannel.VirtualCard:
|
|
3877
|
-
return LokotroPayScreenNavigation.CardFormScreen;
|
|
3878
|
-
case LokotroPayChannel.EFlash:
|
|
3879
|
-
return LokotroPayScreenNavigation.FlashFormScreen;
|
|
3880
|
-
default:
|
|
3881
|
-
return LokotroPayScreenNavigation.PaymentFormScreen;
|
|
4120
|
+
ngOnInit() {
|
|
4121
|
+
this.otpDigits = new Array(this.otpLength).fill('');
|
|
4122
|
+
this.startResendTimer();
|
|
4123
|
+
}
|
|
4124
|
+
ngOnDestroy() {
|
|
4125
|
+
if (this.timerInterval) {
|
|
4126
|
+
clearInterval(this.timerInterval);
|
|
3882
4127
|
}
|
|
3883
4128
|
}
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
if (
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
if (body.successRedirectUrl)
|
|
3911
|
-
request['success_redirect_url'] = body.successRedirectUrl;
|
|
3912
|
-
if (body.failRedirectUrl)
|
|
3913
|
-
request['fail_redirect_url'] = body.failRedirectUrl;
|
|
3914
|
-
if (body.userInfo === 'full') {
|
|
3915
|
-
if (body.firstName)
|
|
3916
|
-
request['first_name'] = body.firstName;
|
|
3917
|
-
if (body.lastName)
|
|
3918
|
-
request['last_name'] = body.lastName;
|
|
3919
|
-
if (body.phoneNumber)
|
|
3920
|
-
request['phone_number'] = body.phoneNumber;
|
|
3921
|
-
if (body.email)
|
|
3922
|
-
request['email'] = body.email;
|
|
4129
|
+
get isOtpComplete() {
|
|
4130
|
+
return this.otpDigits.every(digit => digit !== '');
|
|
4131
|
+
}
|
|
4132
|
+
get otpValue() {
|
|
4133
|
+
return this.otpDigits.join('');
|
|
4134
|
+
}
|
|
4135
|
+
startResendTimer() {
|
|
4136
|
+
this.resendTimer = 60;
|
|
4137
|
+
this.timerInterval = setInterval(() => {
|
|
4138
|
+
this.resendTimer--;
|
|
4139
|
+
if (this.resendTimer <= 0) {
|
|
4140
|
+
clearInterval(this.timerInterval);
|
|
4141
|
+
}
|
|
4142
|
+
}, 1000);
|
|
4143
|
+
}
|
|
4144
|
+
onOtpInput(event, index) {
|
|
4145
|
+
const input = event.target;
|
|
4146
|
+
const value = input.value.replace(/\D/g, '');
|
|
4147
|
+
if (value) {
|
|
4148
|
+
this.otpDigits[index] = value[0];
|
|
4149
|
+
input.value = value[0];
|
|
4150
|
+
// Focus next input
|
|
4151
|
+
if (index < this.otpLength - 1) {
|
|
4152
|
+
const inputs = document.querySelectorAll('.lokotro-otp-input');
|
|
4153
|
+
inputs[index + 1]?.focus();
|
|
4154
|
+
}
|
|
3923
4155
|
}
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
request['wallet_number'] = body.walletNumber;
|
|
3927
|
-
if (body.walletPin)
|
|
3928
|
-
request['wallet_pin'] = body.walletPin;
|
|
3929
|
-
if (body.mobileMoneyPhoneNumber)
|
|
3930
|
-
request['mobile_money_phone_number'] = body.mobileMoneyPhoneNumber;
|
|
3931
|
-
if (body.flashNumber)
|
|
3932
|
-
request['flash_number'] = body.flashNumber;
|
|
3933
|
-
if (body.flashPin)
|
|
3934
|
-
request['flash_pin'] = body.flashPin;
|
|
3935
|
-
if (body.cardNumber)
|
|
3936
|
-
request['card_number'] = body.cardNumber;
|
|
3937
|
-
if (body.cardExpiryDate)
|
|
3938
|
-
request['card_expiry_date'] = body.cardExpiryDate;
|
|
3939
|
-
if (body.cardCvv)
|
|
3940
|
-
request['card_cvv'] = body.cardCvv;
|
|
3941
|
-
if (body.cardHolderName)
|
|
3942
|
-
request['card_holder_name'] = body.cardHolderName;
|
|
4156
|
+
else {
|
|
4157
|
+
this.otpDigits[index] = '';
|
|
3943
4158
|
}
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
};
|
|
4159
|
+
}
|
|
4160
|
+
onOtpKeydown(event, index) {
|
|
4161
|
+
if (event.key === 'Backspace' && !this.otpDigits[index] && index > 0) {
|
|
4162
|
+
const inputs = document.querySelectorAll('.lokotro-otp-input');
|
|
4163
|
+
inputs[index - 1]?.focus();
|
|
3950
4164
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
4165
|
+
}
|
|
4166
|
+
onOtpPaste(event) {
|
|
4167
|
+
event.preventDefault();
|
|
4168
|
+
const pastedData = event.clipboardData?.getData('text').replace(/\D/g, '');
|
|
4169
|
+
if (pastedData) {
|
|
4170
|
+
for (let i = 0; i < Math.min(pastedData.length, this.otpLength); i++) {
|
|
4171
|
+
this.otpDigits[i] = pastedData[i];
|
|
4172
|
+
}
|
|
4173
|
+
const inputs = document.querySelectorAll('.lokotro-otp-input');
|
|
4174
|
+
inputs.forEach((input, i) => {
|
|
4175
|
+
input.value = this.otpDigits[i] || '';
|
|
4176
|
+
});
|
|
4177
|
+
// Focus last filled input or first empty
|
|
4178
|
+
const focusIndex = Math.min(pastedData.length, this.otpLength - 1);
|
|
4179
|
+
inputs[focusIndex]?.focus();
|
|
3953
4180
|
}
|
|
3954
|
-
|
|
3955
|
-
|
|
4181
|
+
}
|
|
4182
|
+
onVerify() {
|
|
4183
|
+
if (this.isOtpComplete) {
|
|
4184
|
+
this.otpVerified.emit(this.otpValue);
|
|
3956
4185
|
}
|
|
3957
|
-
|
|
3958
|
-
|
|
4186
|
+
}
|
|
4187
|
+
onResend() {
|
|
4188
|
+
this.otpDigits = new Array(this.otpLength).fill('');
|
|
4189
|
+
this.startResendTimer();
|
|
4190
|
+
this.resendOtp.emit();
|
|
4191
|
+
}
|
|
4192
|
+
onCancelClick() {
|
|
4193
|
+
this.cancel.emit();
|
|
4194
|
+
}
|
|
4195
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroOtpVerificationComponent, deps: [{ token: LokotroLocalizationService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4196
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: LokotroOtpVerificationComponent, isStandalone: true, selector: "lokotro-otp-verification", inputs: { transactionId: "transactionId", otpDestination: "otpDestination", otpLength: "otpLength" }, outputs: { otpVerified: "otpVerified", resendOtp: "resendOtp", cancel: "cancel" }, ngImport: i0, template: `
|
|
4197
|
+
<div class="lokotro-otp-verification">
|
|
4198
|
+
<div class="lokotro-otp-icon">
|
|
4199
|
+
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
4200
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
4201
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
4202
|
+
</svg>
|
|
4203
|
+
</div>
|
|
4204
|
+
|
|
4205
|
+
<h3 class="lokotro-otp-title">{{ localization.translate('enterOtp') }}</h3>
|
|
4206
|
+
@if (otpDestination) {
|
|
4207
|
+
<p class="lokotro-otp-subtitle">
|
|
4208
|
+
{{ localization.translate('otpSentTo') }} {{ otpDestination }}
|
|
4209
|
+
</p>
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
<div class="lokotro-otp-inputs">
|
|
4213
|
+
@for (digit of otpDigits; track digit; let i = $index) {
|
|
4214
|
+
<input
|
|
4215
|
+
type="text"
|
|
4216
|
+
class="lokotro-otp-input"
|
|
4217
|
+
maxlength="1"
|
|
4218
|
+
[value]="otpDigits[i]"
|
|
4219
|
+
(input)="onOtpInput($event, i)"
|
|
4220
|
+
(keydown)="onOtpKeydown($event, i)"
|
|
4221
|
+
(paste)="onOtpPaste($event)"
|
|
4222
|
+
#otpInput>
|
|
3959
4223
|
}
|
|
3960
|
-
|
|
3961
|
-
|
|
4224
|
+
</div>
|
|
4225
|
+
|
|
4226
|
+
@if (resendTimer > 0) {
|
|
4227
|
+
<div class="lokotro-otp-timer">
|
|
4228
|
+
Resend OTP in {{ resendTimer }}s
|
|
4229
|
+
</div>
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
@if (resendTimer === 0) {
|
|
4233
|
+
<button
|
|
4234
|
+
class="lokotro-resend-btn"
|
|
4235
|
+
(click)="onResend()">
|
|
4236
|
+
{{ localization.translate('resendOtp') }}
|
|
4237
|
+
</button>
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
<div class="lokotro-form-actions">
|
|
4241
|
+
<button type="button" class="lokotro-btn-secondary" (click)="onCancelClick()">
|
|
4242
|
+
{{ localization.translate('cancel') }}
|
|
4243
|
+
</button>
|
|
4244
|
+
<button
|
|
4245
|
+
type="button"
|
|
4246
|
+
class="lokotro-btn-primary"
|
|
4247
|
+
[disabled]="!isOtpComplete"
|
|
4248
|
+
(click)="onVerify()">
|
|
4249
|
+
{{ localization.translate('verifyOtp') }}
|
|
4250
|
+
</button>
|
|
4251
|
+
</div>
|
|
4252
|
+
</div>
|
|
4253
|
+
`, isInline: true, styles: [".lokotro-otp-verification{display:flex;flex-direction:column;align-items:center;text-align:center;padding:24px 0}.lokotro-otp-icon{width:80px;height:80px;display:flex;align-items:center;justify-content:center;background:var(--lokotro-card, #3A4840);border-radius:50%;margin-bottom:24px;color:var(--lokotro-accent, #3BFBDA)}.lokotro-otp-title{font-size:24px;font-weight:600;margin:0 0 8px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-otp-subtitle{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0 0 32px}.lokotro-otp-inputs{display:flex;gap:12px;margin-bottom:24px}.lokotro-otp-input{width:50px;height:56px;text-align:center;font-size:24px;font-weight:600;background:var(--lokotro-card, #3A4840);border:2px solid var(--lokotro-border, #3A473F);border-radius:12px;color:var(--lokotro-text-primary, #F2F0D5);transition:border-color .2s}.lokotro-otp-input:focus{outline:none;border-color:var(--lokotro-accent, #3BFBDA)}.lokotro-otp-timer{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin-bottom:24px}.lokotro-resend-btn{background:none;border:none;color:var(--lokotro-accent, #3BFBDA);font-size:14px;font-weight:600;cursor:pointer;margin-bottom:24px;text-decoration:underline}.lokotro-resend-btn:hover{opacity:.8}.lokotro-form-actions{display:flex;gap:12px;width:100%}.lokotro-btn-primary,.lokotro-btn-secondary{flex:1;padding:14px 24px;border-radius:12px;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s ease}.lokotro-btn-primary{background:linear-gradient(135deg,var(--lokotro-primary, #5A5E39),var(--lokotro-secondary, #6E7346));color:var(--lokotro-text-primary, #F2F0D5);border:none}.lokotro-btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 16px #0003}.lokotro-btn-primary:disabled{opacity:.5;cursor:not-allowed}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #F2F0D5);border:2px solid var(--lokotro-border, #3A473F)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #D5D3B8)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }] }); }
|
|
4254
|
+
}
|
|
4255
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroOtpVerificationComponent, decorators: [{
|
|
4256
|
+
type: Component,
|
|
4257
|
+
args: [{ selector: 'lokotro-otp-verification', standalone: true, imports: [FormsModule], template: `
|
|
4258
|
+
<div class="lokotro-otp-verification">
|
|
4259
|
+
<div class="lokotro-otp-icon">
|
|
4260
|
+
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
4261
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
4262
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
4263
|
+
</svg>
|
|
4264
|
+
</div>
|
|
4265
|
+
|
|
4266
|
+
<h3 class="lokotro-otp-title">{{ localization.translate('enterOtp') }}</h3>
|
|
4267
|
+
@if (otpDestination) {
|
|
4268
|
+
<p class="lokotro-otp-subtitle">
|
|
4269
|
+
{{ localization.translate('otpSentTo') }} {{ otpDestination }}
|
|
4270
|
+
</p>
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
<div class="lokotro-otp-inputs">
|
|
4274
|
+
@for (digit of otpDigits; track digit; let i = $index) {
|
|
4275
|
+
<input
|
|
4276
|
+
type="text"
|
|
4277
|
+
class="lokotro-otp-input"
|
|
4278
|
+
maxlength="1"
|
|
4279
|
+
[value]="otpDigits[i]"
|
|
4280
|
+
(input)="onOtpInput($event, i)"
|
|
4281
|
+
(keydown)="onOtpKeydown($event, i)"
|
|
4282
|
+
(paste)="onOtpPaste($event)"
|
|
4283
|
+
#otpInput>
|
|
3962
4284
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
4285
|
+
</div>
|
|
4286
|
+
|
|
4287
|
+
@if (resendTimer > 0) {
|
|
4288
|
+
<div class="lokotro-otp-timer">
|
|
4289
|
+
Resend OTP in {{ resendTimer }}s
|
|
4290
|
+
</div>
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
@if (resendTimer === 0) {
|
|
4294
|
+
<button
|
|
4295
|
+
class="lokotro-resend-btn"
|
|
4296
|
+
(click)="onResend()">
|
|
4297
|
+
{{ localization.translate('resendOtp') }}
|
|
4298
|
+
</button>
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
<div class="lokotro-form-actions">
|
|
4302
|
+
<button type="button" class="lokotro-btn-secondary" (click)="onCancelClick()">
|
|
4303
|
+
{{ localization.translate('cancel') }}
|
|
4304
|
+
</button>
|
|
4305
|
+
<button
|
|
4306
|
+
type="button"
|
|
4307
|
+
class="lokotro-btn-primary"
|
|
4308
|
+
[disabled]="!isOtpComplete"
|
|
4309
|
+
(click)="onVerify()">
|
|
4310
|
+
{{ localization.translate('verifyOtp') }}
|
|
4311
|
+
</button>
|
|
4312
|
+
</div>
|
|
4313
|
+
</div>
|
|
4314
|
+
`, styles: [".lokotro-otp-verification{display:flex;flex-direction:column;align-items:center;text-align:center;padding:24px 0}.lokotro-otp-icon{width:80px;height:80px;display:flex;align-items:center;justify-content:center;background:var(--lokotro-card, #3A4840);border-radius:50%;margin-bottom:24px;color:var(--lokotro-accent, #3BFBDA)}.lokotro-otp-title{font-size:24px;font-weight:600;margin:0 0 8px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-otp-subtitle{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0 0 32px}.lokotro-otp-inputs{display:flex;gap:12px;margin-bottom:24px}.lokotro-otp-input{width:50px;height:56px;text-align:center;font-size:24px;font-weight:600;background:var(--lokotro-card, #3A4840);border:2px solid var(--lokotro-border, #3A473F);border-radius:12px;color:var(--lokotro-text-primary, #F2F0D5);transition:border-color .2s}.lokotro-otp-input:focus{outline:none;border-color:var(--lokotro-accent, #3BFBDA)}.lokotro-otp-timer{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin-bottom:24px}.lokotro-resend-btn{background:none;border:none;color:var(--lokotro-accent, #3BFBDA);font-size:14px;font-weight:600;cursor:pointer;margin-bottom:24px;text-decoration:underline}.lokotro-resend-btn:hover{opacity:.8}.lokotro-form-actions{display:flex;gap:12px;width:100%}.lokotro-btn-primary,.lokotro-btn-secondary{flex:1;padding:14px 24px;border-radius:12px;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s ease}.lokotro-btn-primary{background:linear-gradient(135deg,var(--lokotro-primary, #5A5E39),var(--lokotro-secondary, #6E7346));color:var(--lokotro-text-primary, #F2F0D5);border:none}.lokotro-btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 16px #0003}.lokotro-btn-primary:disabled{opacity:.5;cursor:not-allowed}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #F2F0D5);border:2px solid var(--lokotro-border, #3A473F)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #D5D3B8)}\n"] }]
|
|
4315
|
+
}], ctorParameters: () => [{ type: LokotroLocalizationService }], propDecorators: { transactionId: [{
|
|
4316
|
+
type: Input
|
|
4317
|
+
}], otpDestination: [{
|
|
4318
|
+
type: Input
|
|
4319
|
+
}], otpLength: [{
|
|
4320
|
+
type: Input
|
|
4321
|
+
}], otpVerified: [{
|
|
4322
|
+
type: Output
|
|
4323
|
+
}], resendOtp: [{
|
|
4324
|
+
type: Output
|
|
4325
|
+
}], cancel: [{
|
|
4326
|
+
type: Output
|
|
4327
|
+
}] } });
|
|
4328
|
+
|
|
4329
|
+
/**
|
|
4330
|
+
* Lokotro Pay - Processing Component
|
|
4331
|
+
*/
|
|
4332
|
+
class LokotroProcessingComponent {
|
|
4333
|
+
constructor(localization) {
|
|
4334
|
+
this.localization = localization;
|
|
4335
|
+
this.type = 'default';
|
|
4336
|
+
this.currentStep = 1;
|
|
4337
|
+
// Simulate step progression for mobile money
|
|
4338
|
+
if (this.type === 'mobileMoney') {
|
|
4339
|
+
setTimeout(() => this.currentStep = 2, 3000);
|
|
4340
|
+
setTimeout(() => this.currentStep = 3, 6000);
|
|
3965
4341
|
}
|
|
3966
|
-
return request;
|
|
3967
|
-
}
|
|
3968
|
-
/**
|
|
3969
|
-
* Convert submit request to API format
|
|
3970
|
-
*/
|
|
3971
|
-
convertSubmitRequestToData(request) {
|
|
3972
|
-
const data = {
|
|
3973
|
-
payment_id: this.resolvePaymentId(request),
|
|
3974
|
-
payment_method_id: request.paymentMethodId || request.paymentMethod || ''
|
|
3975
|
-
};
|
|
3976
|
-
if (request.firstName)
|
|
3977
|
-
data['first_name'] = request.firstName;
|
|
3978
|
-
if (request.lastName)
|
|
3979
|
-
data['last_name'] = request.lastName;
|
|
3980
|
-
if (request.email)
|
|
3981
|
-
data['email'] = request.email;
|
|
3982
|
-
if (request.phoneNumber)
|
|
3983
|
-
data['phone_number'] = request.phoneNumber;
|
|
3984
|
-
if (request.walletNumber)
|
|
3985
|
-
data['ewallet_number'] = request.walletNumber;
|
|
3986
|
-
if (request.walletPin)
|
|
3987
|
-
data['ewallet_pin'] = request.walletPin;
|
|
3988
|
-
if (request.mobileMoneyPhoneNumber)
|
|
3989
|
-
data['mobile_money_phone_number'] = request.mobileMoneyPhoneNumber;
|
|
3990
|
-
if (request.flashNumber)
|
|
3991
|
-
data['flash_number'] = request.flashNumber;
|
|
3992
|
-
if (request.flashPin)
|
|
3993
|
-
data['flash_pin'] = request.flashPin;
|
|
3994
|
-
if (request.cardNumber)
|
|
3995
|
-
data['card_number'] = request.cardNumber;
|
|
3996
|
-
if (request.cardExpiryDate)
|
|
3997
|
-
data['card_expiry_date'] = request.cardExpiryDate;
|
|
3998
|
-
if (request.cardCvv)
|
|
3999
|
-
data['card_cvv'] = request.cardCvv;
|
|
4000
|
-
if (request.cardHolderName)
|
|
4001
|
-
data['card_holder_name'] = request.cardHolderName;
|
|
4002
|
-
if (request.bankTransferAccountId)
|
|
4003
|
-
data['bank_account_id'] = request.bankTransferAccountId;
|
|
4004
|
-
if (request.bankAccountId)
|
|
4005
|
-
data['bank_account_id'] = request.bankAccountId;
|
|
4006
|
-
if (request.bankAccountNumber)
|
|
4007
|
-
data['bank_account_number'] = request.bankAccountNumber;
|
|
4008
|
-
if (request.bankAccountLabel)
|
|
4009
|
-
data['bank_account_label'] = request.bankAccountLabel;
|
|
4010
|
-
if (request.bankId)
|
|
4011
|
-
data['bank_id'] = request.bankId;
|
|
4012
|
-
if (request.bankEntityId)
|
|
4013
|
-
data['bank_entity_id'] = request.bankEntityId;
|
|
4014
|
-
if (request.mastercardPaymentMethod)
|
|
4015
|
-
data['mastercard_payment_method'] = request.mastercardPaymentMethod;
|
|
4016
|
-
if (request.sessionId)
|
|
4017
|
-
data['session_id'] = request.sessionId;
|
|
4018
|
-
if (request.cardData)
|
|
4019
|
-
data['card_data'] = request.cardData;
|
|
4020
|
-
if (typeof request.savePaymentMethod === 'boolean')
|
|
4021
|
-
data['save_payment_method'] = request.savePaymentMethod;
|
|
4022
|
-
return data;
|
|
4023
|
-
}
|
|
4024
|
-
/**
|
|
4025
|
-
* Parse payment info from collect response.
|
|
4026
|
-
*/
|
|
4027
|
-
parsePaymentInfo(data) {
|
|
4028
|
-
const additionalData = this.asRecord(data['additional_data']);
|
|
4029
|
-
// Drop feature-flag-hidden methods (e.g. Google/Apple Pay) so they never
|
|
4030
|
-
// enter the payment info model, get rendered, or get auto-selected.
|
|
4031
|
-
const availablePaymentMethods = visiblePaymentMethods(this.getRecordArray(data['available_payment_methods'] || additionalData?.['payment_methods'])
|
|
4032
|
-
.map(method => this.parsePaymentMethod(method)));
|
|
4033
|
-
const action = this.getString(additionalData?.['action']);
|
|
4034
|
-
return {
|
|
4035
|
-
id: this.resolvePaymentId(data),
|
|
4036
|
-
amount: this.getNumber(data['amount']),
|
|
4037
|
-
currency: this.getString(data['currency']) || 'USD',
|
|
4038
|
-
description: this.getString(data['description']),
|
|
4039
|
-
merchantName: this.getString(data['merchant_name']),
|
|
4040
|
-
merchantId: this.getString(data['merchant_id']),
|
|
4041
|
-
availablePaymentMethods,
|
|
4042
|
-
createdAt: new Date(this.getString(data['created_at']) || Date.now()),
|
|
4043
|
-
expiresAt: this.getString(data['expires_at']) ? new Date(this.getString(data['expires_at'])) : undefined,
|
|
4044
|
-
metadata: this.asRecord(data['metadata']),
|
|
4045
|
-
paymentUrl: this.getString(data['payment_url']) || undefined,
|
|
4046
|
-
showUserInfoForm: this.getBoolean(data['show_user_info_form']) || action === 'select_payment_method',
|
|
4047
|
-
showPaymentMethodForm: this.getBoolean(data['show_payment_method_form']) || action === 'select_payment_method'
|
|
4048
|
-
};
|
|
4049
|
-
}
|
|
4050
|
-
/**
|
|
4051
|
-
* Parse payment info from transaction details response.
|
|
4052
|
-
*/
|
|
4053
|
-
parseTransactionPaymentInfo(data) {
|
|
4054
|
-
const fillingInfo = this.getString(data['filling_info']) || 'none';
|
|
4055
|
-
const channelInfo = this.getString(data['channel_info']) || 'none';
|
|
4056
|
-
return {
|
|
4057
|
-
id: this.resolvePaymentId(data),
|
|
4058
|
-
amount: this.getNumber(data['transactional_amount'] ?? data['amount']),
|
|
4059
|
-
currency: this.getString(data['transactional_currency']) || this.getString(data['currency_str']) || 'USD',
|
|
4060
|
-
description: this.getString(data['description']),
|
|
4061
|
-
merchantName: this.getString(data['merchant_name']),
|
|
4062
|
-
merchantId: this.getString(data['merchant_id']),
|
|
4063
|
-
availablePaymentMethods: visiblePaymentMethods(this.getRecordArray(data['payment_methods']).map(method => this.parsePaymentMethod(method))),
|
|
4064
|
-
createdAt: new Date(this.getString(data['created_at']) || Date.now()),
|
|
4065
|
-
metadata: this.asRecord(data['metadata']),
|
|
4066
|
-
paymentUrl: this.getString(data['payment_url']) || undefined,
|
|
4067
|
-
showUserInfoForm: fillingInfo === 'none',
|
|
4068
|
-
showPaymentMethodForm: channelInfo === 'none',
|
|
4069
|
-
fillingInfo,
|
|
4070
|
-
channelInfo
|
|
4071
|
-
};
|
|
4072
4342
|
}
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4343
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroProcessingComponent, deps: [{ token: LokotroLocalizationService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4344
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: LokotroProcessingComponent, isStandalone: true, selector: "lokotro-processing", inputs: { type: "type", message: "message" }, ngImport: i0, template: `
|
|
4345
|
+
<div class="lokotro-processing">
|
|
4346
|
+
<div class="lokotro-processing-spinner">
|
|
4347
|
+
<div class="lokotro-spinner-ring"></div>
|
|
4348
|
+
<div class="lokotro-spinner-ring"></div>
|
|
4349
|
+
<div class="lokotro-spinner-ring"></div>
|
|
4350
|
+
</div>
|
|
4351
|
+
|
|
4352
|
+
<h3 class="lokotro-processing-title">{{ message || localization.translate('processing') }}</h3>
|
|
4353
|
+
|
|
4354
|
+
@if (type === 'mobileMoney') {
|
|
4355
|
+
<p class="lokotro-processing-subtitle">
|
|
4356
|
+
Please confirm the payment on your mobile device
|
|
4357
|
+
</p>
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
@if (type === 'mobileMoney') {
|
|
4361
|
+
<div class="lokotro-processing-steps">
|
|
4362
|
+
<div class="lokotro-step" [class.active]="currentStep >= 1">
|
|
4363
|
+
<div class="lokotro-step-icon">📱</div>
|
|
4364
|
+
<span>Check your phone</span>
|
|
4365
|
+
</div>
|
|
4366
|
+
<div class="lokotro-step" [class.active]="currentStep >= 2">
|
|
4367
|
+
<div class="lokotro-step-icon">🔐</div>
|
|
4368
|
+
<span>Enter your PIN</span>
|
|
4369
|
+
</div>
|
|
4370
|
+
<div class="lokotro-step" [class.active]="currentStep >= 3">
|
|
4371
|
+
<div class="lokotro-step-icon">✅</div>
|
|
4372
|
+
<span>Confirm payment</span>
|
|
4373
|
+
</div>
|
|
4374
|
+
</div>
|
|
4375
|
+
}
|
|
4376
|
+
</div>
|
|
4377
|
+
`, isInline: true, styles: [".lokotro-processing{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:48px 24px;min-height:300px}.lokotro-processing-spinner{position:relative;width:80px;height:80px;margin-bottom:32px}.lokotro-spinner-ring{position:absolute;width:100%;height:100%;border:3px solid transparent;border-radius:50%;animation:lokotro-spin 1.5s linear infinite}.lokotro-spinner-ring:nth-child(1){border-top-color:var(--lokotro-accent, #3BFBDA);animation-delay:0s}.lokotro-spinner-ring:nth-child(2){border-right-color:var(--lokotro-primary, #5A5E39);animation-delay:.15s;width:70%;height:70%;top:15%;left:15%}.lokotro-spinner-ring:nth-child(3){border-bottom-color:var(--lokotro-secondary, #6E7346);animation-delay:.3s;width:50%;height:50%;top:25%;left:25%}@keyframes lokotro-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.lokotro-processing-title{font-size:20px;font-weight:600;margin:0 0 8px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-processing-subtitle{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0 0 32px}.lokotro-processing-steps{display:flex;flex-direction:column;gap:16px;width:100%;max-width:280px}.lokotro-step{display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--lokotro-card, #3A4840);border-radius:12px;opacity:.5;transition:opacity .3s}.lokotro-step.active{opacity:1}.lokotro-step-icon{font-size:20px}.lokotro-step span{font-size:14px;color:var(--lokotro-text-primary, #F2F0D5)}\n"] }); }
|
|
4378
|
+
}
|
|
4379
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroProcessingComponent, decorators: [{
|
|
4380
|
+
type: Component,
|
|
4381
|
+
args: [{ selector: 'lokotro-processing', standalone: true, imports: [], template: `
|
|
4382
|
+
<div class="lokotro-processing">
|
|
4383
|
+
<div class="lokotro-processing-spinner">
|
|
4384
|
+
<div class="lokotro-spinner-ring"></div>
|
|
4385
|
+
<div class="lokotro-spinner-ring"></div>
|
|
4386
|
+
<div class="lokotro-spinner-ring"></div>
|
|
4387
|
+
</div>
|
|
4388
|
+
|
|
4389
|
+
<h3 class="lokotro-processing-title">{{ message || localization.translate('processing') }}</h3>
|
|
4390
|
+
|
|
4391
|
+
@if (type === 'mobileMoney') {
|
|
4392
|
+
<p class="lokotro-processing-subtitle">
|
|
4393
|
+
Please confirm the payment on your mobile device
|
|
4394
|
+
</p>
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
@if (type === 'mobileMoney') {
|
|
4398
|
+
<div class="lokotro-processing-steps">
|
|
4399
|
+
<div class="lokotro-step" [class.active]="currentStep >= 1">
|
|
4400
|
+
<div class="lokotro-step-icon">📱</div>
|
|
4401
|
+
<span>Check your phone</span>
|
|
4402
|
+
</div>
|
|
4403
|
+
<div class="lokotro-step" [class.active]="currentStep >= 2">
|
|
4404
|
+
<div class="lokotro-step-icon">🔐</div>
|
|
4405
|
+
<span>Enter your PIN</span>
|
|
4406
|
+
</div>
|
|
4407
|
+
<div class="lokotro-step" [class.active]="currentStep >= 3">
|
|
4408
|
+
<div class="lokotro-step-icon">✅</div>
|
|
4409
|
+
<span>Confirm payment</span>
|
|
4410
|
+
</div>
|
|
4411
|
+
</div>
|
|
4412
|
+
}
|
|
4413
|
+
</div>
|
|
4414
|
+
`, styles: [".lokotro-processing{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:48px 24px;min-height:300px}.lokotro-processing-spinner{position:relative;width:80px;height:80px;margin-bottom:32px}.lokotro-spinner-ring{position:absolute;width:100%;height:100%;border:3px solid transparent;border-radius:50%;animation:lokotro-spin 1.5s linear infinite}.lokotro-spinner-ring:nth-child(1){border-top-color:var(--lokotro-accent, #3BFBDA);animation-delay:0s}.lokotro-spinner-ring:nth-child(2){border-right-color:var(--lokotro-primary, #5A5E39);animation-delay:.15s;width:70%;height:70%;top:15%;left:15%}.lokotro-spinner-ring:nth-child(3){border-bottom-color:var(--lokotro-secondary, #6E7346);animation-delay:.3s;width:50%;height:50%;top:25%;left:25%}@keyframes lokotro-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.lokotro-processing-title{font-size:20px;font-weight:600;margin:0 0 8px;color:var(--lokotro-text-primary, #F2F0D5)}.lokotro-processing-subtitle{font-size:14px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0 0 32px}.lokotro-processing-steps{display:flex;flex-direction:column;gap:16px;width:100%;max-width:280px}.lokotro-step{display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--lokotro-card, #3A4840);border-radius:12px;opacity:.5;transition:opacity .3s}.lokotro-step.active{opacity:1}.lokotro-step-icon{font-size:20px}.lokotro-step span{font-size:14px;color:var(--lokotro-text-primary, #F2F0D5)}\n"] }]
|
|
4415
|
+
}], ctorParameters: () => [{ type: LokotroLocalizationService }], propDecorators: { type: [{
|
|
4416
|
+
type: Input
|
|
4417
|
+
}], message: [{
|
|
4418
|
+
type: Input
|
|
4419
|
+
}] } });
|
|
4420
|
+
|
|
4421
|
+
/**
|
|
4422
|
+
* Lokotro Pay - Result Component
|
|
4423
|
+
* Success, Error, Warning, Info screens
|
|
4424
|
+
*
|
|
4425
|
+
* Designed for financial UX: clean white surface, prominent amount focal
|
|
4426
|
+
* on success, filled status badge, restrained transaction-detail panel.
|
|
4427
|
+
*/
|
|
4428
|
+
class LokotroResultComponent {
|
|
4429
|
+
constructor(localization) {
|
|
4430
|
+
this.localization = localization;
|
|
4431
|
+
this.type = 'success';
|
|
4432
|
+
this.title = '';
|
|
4433
|
+
/** Auto-redirect duration in seconds. If > 0, will auto-call onAutoRedirect after countdown. */
|
|
4434
|
+
this.autoRedirectSeconds = 0;
|
|
4435
|
+
this.primaryAction = new EventEmitter();
|
|
4436
|
+
this.secondaryAction = new EventEmitter();
|
|
4437
|
+
/** Emitted when auto-redirect triggers after countdown completes. */
|
|
4438
|
+
this.autoRedirect = new EventEmitter();
|
|
4439
|
+
// Countdown state
|
|
4440
|
+
this.countdownSeconds = 0;
|
|
4441
|
+
this.isCountdownCancelled = false;
|
|
4099
4442
|
}
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
* Unknown / empty inputs return undefined so the field stays optional.
|
|
4103
|
-
*/
|
|
4104
|
-
parsePaymentMethodFlag(value) {
|
|
4105
|
-
const known = [
|
|
4106
|
-
'none', 'all',
|
|
4107
|
-
'onafriq_mobile_money', 'onafriq_credit_card', 'rawbank_credit_card',
|
|
4108
|
-
'lokotro_wallet', 'lokotro_eflash',
|
|
4109
|
-
'cash', 'bank_transfer', 'google_pay', 'apple_pay',
|
|
4110
|
-
];
|
|
4111
|
-
return known.find(f => f === value);
|
|
4443
|
+
ngOnInit() {
|
|
4444
|
+
this.startAutoRedirectCountdown();
|
|
4112
4445
|
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
*/
|
|
4116
|
-
parseSubmitResponse(data) {
|
|
4117
|
-
const rawStatus = this.getString(data['status']);
|
|
4118
|
-
return {
|
|
4119
|
-
success: this.getBoolean(data['success']),
|
|
4120
|
-
message: this.getString(data['message']),
|
|
4121
|
-
transactionId: this.resolvePaymentId(data),
|
|
4122
|
-
status: LokotroPaymentStatusInfo.fromString(rawStatus),
|
|
4123
|
-
requiresOtp: this.getBoolean(data['requires_otp']) || rawStatus === 'pending_otp' || rawStatus === 'pending_otp_verification',
|
|
4124
|
-
otpDestination: this.getString(data['otp_destination']) || this.getString(data['otp_sent_to']) || undefined,
|
|
4125
|
-
redirectUrl: this.getString(data['redirect_url']) || this.getString(data['mastercardUrl']) || undefined
|
|
4126
|
-
};
|
|
4446
|
+
ngOnDestroy() {
|
|
4447
|
+
this.clearCountdown();
|
|
4127
4448
|
}
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
flashNumber: paymentBody.flashNumber,
|
|
4144
|
-
flashPin: paymentBody.flashPin,
|
|
4145
|
-
cardNumber: paymentBody.cardNumber,
|
|
4146
|
-
cardExpiryDate: paymentBody.cardExpiryDate,
|
|
4147
|
-
cardCvv: paymentBody.cardCvv,
|
|
4148
|
-
cardHolderName: paymentBody.cardHolderName,
|
|
4149
|
-
mastercardPaymentMethod: paymentBody.mastercardPaymentMethod
|
|
4150
|
-
};
|
|
4449
|
+
startAutoRedirectCountdown() {
|
|
4450
|
+
if (this.autoRedirectSeconds <= 0)
|
|
4451
|
+
return;
|
|
4452
|
+
this.countdownSeconds = this.autoRedirectSeconds;
|
|
4453
|
+
this.countdownInterval = setInterval(() => {
|
|
4454
|
+
if (this.isCountdownCancelled) {
|
|
4455
|
+
this.clearCountdown();
|
|
4456
|
+
return;
|
|
4457
|
+
}
|
|
4458
|
+
this.countdownSeconds--;
|
|
4459
|
+
if (this.countdownSeconds <= 0) {
|
|
4460
|
+
this.clearCountdown();
|
|
4461
|
+
this.triggerAutoRedirect();
|
|
4462
|
+
}
|
|
4463
|
+
}, 1000);
|
|
4151
4464
|
}
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
const paymentMethodId = this.getString(transactionData['payment_method_id']);
|
|
4157
|
-
const paymentMethodName = this.getString(transactionData['payment_method_str']) || this.getString(transactionData['payment_method']);
|
|
4158
|
-
const normalizedName = this.normalizeMethodKey(paymentMethodName);
|
|
4159
|
-
const exactMatch = paymentInfo.availablePaymentMethods.find(method => method.id === paymentMethodId ||
|
|
4160
|
-
this.normalizeMethodKey(method.name) === normalizedName ||
|
|
4161
|
-
this.normalizeMethodKey(method.displayName) === normalizedName);
|
|
4162
|
-
if (exactMatch) {
|
|
4163
|
-
return exactMatch;
|
|
4164
|
-
}
|
|
4165
|
-
const channel = this.parseChannel(this.getString(transactionData['channel']) || paymentMethodName);
|
|
4166
|
-
if (!paymentMethodId && !paymentMethodName && channel === LokotroPayChannel.None) {
|
|
4167
|
-
return paymentInfo.availablePaymentMethods[0];
|
|
4465
|
+
cancelCountdown() {
|
|
4466
|
+
if (this.countdownInterval) {
|
|
4467
|
+
this.isCountdownCancelled = true;
|
|
4468
|
+
this.clearCountdown();
|
|
4168
4469
|
}
|
|
4169
|
-
return {
|
|
4170
|
-
id: paymentMethodId || paymentMethodName || channel,
|
|
4171
|
-
name: paymentMethodName || paymentMethodId || channel,
|
|
4172
|
-
displayName: paymentMethodName || paymentMethodId || channel,
|
|
4173
|
-
channel,
|
|
4174
|
-
iconUrl: '',
|
|
4175
|
-
isEnabled: true
|
|
4176
|
-
};
|
|
4177
4470
|
}
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
const record = this.asRecord(payload);
|
|
4183
|
-
if (!record) {
|
|
4184
|
-
return {};
|
|
4471
|
+
clearCountdown() {
|
|
4472
|
+
if (this.countdownInterval) {
|
|
4473
|
+
clearInterval(this.countdownInterval);
|
|
4474
|
+
this.countdownInterval = undefined;
|
|
4185
4475
|
}
|
|
4186
|
-
const nestedData = this.asRecord(record['data']);
|
|
4187
|
-
return nestedData || record;
|
|
4188
4476
|
}
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
this.getString(source['transactionId']) ||
|
|
4194
|
-
this.currentState.transactionId ||
|
|
4195
|
-
'';
|
|
4477
|
+
triggerAutoRedirect() {
|
|
4478
|
+
if (this.isCountdownCancelled)
|
|
4479
|
+
return;
|
|
4480
|
+
this.autoRedirect.emit();
|
|
4196
4481
|
}
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4482
|
+
getCountdownProgress() {
|
|
4483
|
+
if (this.autoRedirectSeconds <= 0)
|
|
4484
|
+
return 0;
|
|
4485
|
+
return (this.countdownSeconds / this.autoRedirectSeconds) * 100;
|
|
4486
|
+
}
|
|
4487
|
+
formatNumber(amount) {
|
|
4488
|
+
return new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount);
|
|
4489
|
+
}
|
|
4490
|
+
formatAmount(amount, currency) {
|
|
4491
|
+
return new Intl.NumberFormat('en-US', {
|
|
4492
|
+
style: 'currency',
|
|
4493
|
+
currency: currency || 'USD',
|
|
4494
|
+
minimumFractionDigits: 2
|
|
4495
|
+
}).format(amount);
|
|
4496
|
+
}
|
|
4497
|
+
onPrimaryClick() {
|
|
4498
|
+
this.cancelCountdown();
|
|
4499
|
+
this.primaryAction.emit();
|
|
4500
|
+
}
|
|
4501
|
+
onSecondaryClick() {
|
|
4502
|
+
this.cancelCountdown();
|
|
4503
|
+
this.secondaryAction.emit();
|
|
4504
|
+
}
|
|
4505
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroResultComponent, deps: [{ token: LokotroLocalizationService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4506
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: LokotroResultComponent, isStandalone: true, selector: "lokotro-result", inputs: { type: "type", title: "title", message: "message", amount: "amount", currency: "currency", transactionId: "transactionId", primaryActionLabel: "primaryActionLabel", secondaryActionLabel: "secondaryActionLabel", autoRedirectSeconds: "autoRedirectSeconds" }, outputs: { primaryAction: "primaryAction", secondaryAction: "secondaryAction", autoRedirect: "autoRedirect" }, ngImport: i0, template: `
|
|
4507
|
+
<div class="lokotro-result" [class]="'lokotro-result-' + type">
|
|
4508
|
+
<div class="lokotro-result-card">
|
|
4509
|
+
<!-- Status Badge (filled circle with icon) -->
|
|
4510
|
+
<div class="lokotro-status-badge" [class]="'lokotro-status-' + type">
|
|
4511
|
+
@if (type === 'success') {
|
|
4512
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
4513
|
+
<polyline points="5 12 10 17 19 8"/>
|
|
4514
|
+
</svg>
|
|
4515
|
+
}
|
|
4516
|
+
@if (type === 'error') {
|
|
4517
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
4518
|
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
4519
|
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
4520
|
+
</svg>
|
|
4521
|
+
}
|
|
4522
|
+
@if (type === 'warning') {
|
|
4523
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
4524
|
+
<line x1="12" y1="8" x2="12" y2="13"/>
|
|
4525
|
+
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
4526
|
+
</svg>
|
|
4527
|
+
}
|
|
4528
|
+
@if (type === 'info') {
|
|
4529
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
4530
|
+
<line x1="12" y1="11" x2="12" y2="16"/>
|
|
4531
|
+
<line x1="12" y1="7" x2="12.01" y2="7"/>
|
|
4532
|
+
</svg>
|
|
4533
|
+
}
|
|
4534
|
+
</div>
|
|
4535
|
+
|
|
4536
|
+
<!-- Amount focal (success only, when amount provided) -->
|
|
4537
|
+
@if (type === 'success' && amount !== undefined && amount !== null) {
|
|
4538
|
+
<div class="lokotro-amount-focal">
|
|
4539
|
+
<span class="lokotro-amount-currency">{{ (currency || '').toUpperCase() }}</span>
|
|
4540
|
+
<span class="lokotro-amount-value">{{ formatNumber(amount) }}</span>
|
|
4541
|
+
</div>
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
<!-- Title -->
|
|
4545
|
+
<h2 class="lokotro-result-title">{{ title }}</h2>
|
|
4546
|
+
|
|
4547
|
+
<!-- Message -->
|
|
4548
|
+
@if (message) {
|
|
4549
|
+
<p class="lokotro-result-message">{{ message }}</p>
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
<!-- Countdown Indicator -->
|
|
4553
|
+
@if (autoRedirectSeconds > 0 && countdownSeconds > 0 && !isCountdownCancelled) {
|
|
4554
|
+
<button type="button" class="lokotro-countdown-indicator" (click)="cancelCountdown()">
|
|
4555
|
+
<svg viewBox="0 0 36 36" class="lokotro-countdown-circle">
|
|
4556
|
+
<path class="lokotro-countdown-bg"
|
|
4557
|
+
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
|
4558
|
+
<path class="lokotro-countdown-fill" [class]="'lokotro-countdown-' + type"
|
|
4559
|
+
[attr.stroke-dasharray]="getCountdownProgress() + ', 100'"
|
|
4560
|
+
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
|
4561
|
+
</svg>
|
|
4562
|
+
<span class="lokotro-countdown-text">
|
|
4563
|
+
{{ localization.translate('redirectingIn', { seconds: countdownSeconds }) }}
|
|
4564
|
+
</span>
|
|
4565
|
+
<svg class="lokotro-countdown-cancel" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
4566
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
4567
|
+
</svg>
|
|
4568
|
+
</button>
|
|
4214
4569
|
}
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4570
|
+
|
|
4571
|
+
<!-- Transaction Details -->
|
|
4572
|
+
@if (type === 'success' && transactionId) {
|
|
4573
|
+
<div class="lokotro-result-details">
|
|
4574
|
+
<div class="lokotro-detail-row">
|
|
4575
|
+
<span class="lokotro-detail-label">Transaction ID</span>
|
|
4576
|
+
<span class="lokotro-detail-value lokotro-mono">{{ transactionId }}</span>
|
|
4577
|
+
</div>
|
|
4578
|
+
</div>
|
|
4222
4579
|
}
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4580
|
+
</div>
|
|
4581
|
+
|
|
4582
|
+
<!-- Actions -->
|
|
4583
|
+
@if (primaryActionLabel || secondaryActionLabel) {
|
|
4584
|
+
<div class="lokotro-result-actions">
|
|
4585
|
+
@if (primaryActionLabel) {
|
|
4586
|
+
<button type="button" class="lokotro-btn-primary" (click)="onPrimaryClick()">
|
|
4587
|
+
{{ primaryActionLabel }}
|
|
4588
|
+
</button>
|
|
4589
|
+
}
|
|
4590
|
+
@if (secondaryActionLabel) {
|
|
4591
|
+
<button type="button" class="lokotro-btn-secondary" (click)="onSecondaryClick()">
|
|
4592
|
+
{{ secondaryActionLabel }}
|
|
4593
|
+
</button>
|
|
4594
|
+
}
|
|
4595
|
+
</div>
|
|
4596
|
+
}
|
|
4597
|
+
</div>
|
|
4598
|
+
`, isInline: true, styles: [".lokotro-result{display:flex;flex-direction:column;align-items:stretch;max-width:420px;margin:0 auto;padding:24px 16px;gap:20px}.lokotro-result-card{display:flex;flex-direction:column;align-items:center;text-align:center;padding:32px 24px;background:var(--lokotro-card, #FFFFFF);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:20px;box-shadow:0 1px 3px var(--lokotro-shadow-light, rgba(15, 23, 42, .04)),0 8px 24px var(--lokotro-shadow-light, rgba(15, 23, 42, .04))}.lokotro-status-badge{width:72px;height:72px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:24px;animation:lokotro-pop .5s cubic-bezier(.34,1.56,.64,1)}.lokotro-status-badge svg{width:36px;height:36px;color:#fff}.lokotro-status-success{background:var(--lokotro-success, #16A34A);box-shadow:0 0 0 8px #16a34a1f,0 8px 20px #16a34a40}.lokotro-status-error{background:var(--lokotro-error, #DC2626);box-shadow:0 0 0 8px #dc26261f,0 8px 20px #dc262640}.lokotro-status-warning{background:var(--lokotro-warning, #D97706);box-shadow:0 0 0 8px #d977061f,0 8px 20px #d9770640}.lokotro-status-info{background:var(--lokotro-info, #2563EB);box-shadow:0 0 0 8px #2563eb1f,0 8px 20px #2563eb40}@keyframes lokotro-pop{0%{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.lokotro-amount-focal{display:flex;flex-direction:column;align-items:center;gap:4px;margin-bottom:16px}.lokotro-amount-currency{font-size:12px;font-weight:600;letter-spacing:.08em;color:var(--lokotro-text-secondary, #64748B)}.lokotro-amount-value{font-size:40px;font-weight:700;line-height:1;color:var(--lokotro-text-primary, #0F172A);font-variant-numeric:tabular-nums}.lokotro-result-title{font-size:20px;font-weight:600;margin:0 0 6px;color:var(--lokotro-text-primary, #0F172A)}.lokotro-result-message{font-size:14px;color:var(--lokotro-text-secondary, #64748B);margin:0;line-height:1.5;max-width:320px}.lokotro-result-details{width:100%;background:var(--lokotro-surface, #F7F8FA);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:12px;padding:12px 16px;margin-top:24px}.lokotro-detail-row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:6px 0}.lokotro-detail-row:not(:last-child){border-bottom:1px solid var(--lokotro-divider, #F1F5F9)}.lokotro-detail-label{font-size:13px;color:var(--lokotro-text-secondary, #64748B)}.lokotro-detail-value{font-size:13px;font-weight:600;color:var(--lokotro-text-primary, #0F172A);text-align:right;overflow:hidden;text-overflow:ellipsis;max-width:200px;white-space:nowrap}.lokotro-mono{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Monaco,monospace;font-size:12px}.lokotro-result-actions{display:flex;flex-direction:column;gap:10px;width:100%}.lokotro-btn-primary,.lokotro-btn-secondary{width:100%;padding:14px 24px;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;transition:all .18s ease;font-family:inherit}.lokotro-btn-primary{background:var(--lokotro-primary, #0F172A);color:#fff;border:none}.lokotro-btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 16px var(--lokotro-shadow-medium, rgba(15, 23, 42, .12))}.lokotro-btn-primary:active{transform:translateY(0)}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #0F172A);border:1.5px solid var(--lokotro-border, #E2E8F0)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #64748B);background:var(--lokotro-surface, #F7F8FA)}.lokotro-countdown-indicator{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;background:var(--lokotro-surface, #F7F8FA);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:999px;cursor:pointer;margin-top:20px;transition:all .18s ease;color:var(--lokotro-text-secondary, #64748B);font-family:inherit}.lokotro-countdown-indicator:hover{background:var(--lokotro-card, #FFFFFF);border-color:var(--lokotro-text-tertiary, #64748B)}.lokotro-countdown-circle{width:14px;height:14px;transform:rotate(-90deg)}.lokotro-countdown-bg{fill:none;stroke:var(--lokotro-border, #E2E8F0);stroke-width:4}.lokotro-countdown-fill{fill:none;stroke-width:4;stroke-linecap:round;transition:stroke-dasharray .3s ease}.lokotro-countdown-success{stroke:var(--lokotro-success, #16A34A)}.lokotro-countdown-error{stroke:var(--lokotro-error, #DC2626)}.lokotro-countdown-warning{stroke:var(--lokotro-warning, #D97706)}.lokotro-countdown-info{stroke:var(--lokotro-info, #2563EB)}.lokotro-countdown-text{font-size:12px;font-weight:500}.lokotro-countdown-cancel{color:inherit;opacity:.7}\n"] }); }
|
|
4599
|
+
}
|
|
4600
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroResultComponent, decorators: [{
|
|
4601
|
+
type: Component,
|
|
4602
|
+
args: [{ selector: 'lokotro-result', standalone: true, imports: [], template: `
|
|
4603
|
+
<div class="lokotro-result" [class]="'lokotro-result-' + type">
|
|
4604
|
+
<div class="lokotro-result-card">
|
|
4605
|
+
<!-- Status Badge (filled circle with icon) -->
|
|
4606
|
+
<div class="lokotro-status-badge" [class]="'lokotro-status-' + type">
|
|
4607
|
+
@if (type === 'success') {
|
|
4608
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
4609
|
+
<polyline points="5 12 10 17 19 8"/>
|
|
4610
|
+
</svg>
|
|
4611
|
+
}
|
|
4612
|
+
@if (type === 'error') {
|
|
4613
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
4614
|
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
4615
|
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
4616
|
+
</svg>
|
|
4617
|
+
}
|
|
4618
|
+
@if (type === 'warning') {
|
|
4619
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
4620
|
+
<line x1="12" y1="8" x2="12" y2="13"/>
|
|
4621
|
+
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
4622
|
+
</svg>
|
|
4623
|
+
}
|
|
4624
|
+
@if (type === 'info') {
|
|
4625
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
4626
|
+
<line x1="12" y1="11" x2="12" y2="16"/>
|
|
4627
|
+
<line x1="12" y1="7" x2="12.01" y2="7"/>
|
|
4628
|
+
</svg>
|
|
4629
|
+
}
|
|
4630
|
+
</div>
|
|
4631
|
+
|
|
4632
|
+
<!-- Amount focal (success only, when amount provided) -->
|
|
4633
|
+
@if (type === 'success' && amount !== undefined && amount !== null) {
|
|
4634
|
+
<div class="lokotro-amount-focal">
|
|
4635
|
+
<span class="lokotro-amount-currency">{{ (currency || '').toUpperCase() }}</span>
|
|
4636
|
+
<span class="lokotro-amount-value">{{ formatNumber(amount) }}</span>
|
|
4637
|
+
</div>
|
|
4226
4638
|
}
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
}
|
|
4235
|
-
asRecord(value) {
|
|
4236
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
4237
|
-
? value
|
|
4238
|
-
: undefined;
|
|
4239
|
-
}
|
|
4240
|
-
getRecordArray(value) {
|
|
4241
|
-
if (!Array.isArray(value)) {
|
|
4242
|
-
return [];
|
|
4639
|
+
|
|
4640
|
+
<!-- Title -->
|
|
4641
|
+
<h2 class="lokotro-result-title">{{ title }}</h2>
|
|
4642
|
+
|
|
4643
|
+
<!-- Message -->
|
|
4644
|
+
@if (message) {
|
|
4645
|
+
<p class="lokotro-result-message">{{ message }}</p>
|
|
4243
4646
|
}
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4647
|
+
|
|
4648
|
+
<!-- Countdown Indicator -->
|
|
4649
|
+
@if (autoRedirectSeconds > 0 && countdownSeconds > 0 && !isCountdownCancelled) {
|
|
4650
|
+
<button type="button" class="lokotro-countdown-indicator" (click)="cancelCountdown()">
|
|
4651
|
+
<svg viewBox="0 0 36 36" class="lokotro-countdown-circle">
|
|
4652
|
+
<path class="lokotro-countdown-bg"
|
|
4653
|
+
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
|
4654
|
+
<path class="lokotro-countdown-fill" [class]="'lokotro-countdown-' + type"
|
|
4655
|
+
[attr.stroke-dasharray]="getCountdownProgress() + ', 100'"
|
|
4656
|
+
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
|
4657
|
+
</svg>
|
|
4658
|
+
<span class="lokotro-countdown-text">
|
|
4659
|
+
{{ localization.translate('redirectingIn', { seconds: countdownSeconds }) }}
|
|
4660
|
+
</span>
|
|
4661
|
+
<svg class="lokotro-countdown-cancel" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
4662
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
4663
|
+
</svg>
|
|
4664
|
+
</button>
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
<!-- Transaction Details -->
|
|
4668
|
+
@if (type === 'success' && transactionId) {
|
|
4669
|
+
<div class="lokotro-result-details">
|
|
4670
|
+
<div class="lokotro-detail-row">
|
|
4671
|
+
<span class="lokotro-detail-label">Transaction ID</span>
|
|
4672
|
+
<span class="lokotro-detail-value lokotro-mono">{{ transactionId }}</span>
|
|
4673
|
+
</div>
|
|
4674
|
+
</div>
|
|
4675
|
+
}
|
|
4676
|
+
</div>
|
|
4677
|
+
|
|
4678
|
+
<!-- Actions -->
|
|
4679
|
+
@if (primaryActionLabel || secondaryActionLabel) {
|
|
4680
|
+
<div class="lokotro-result-actions">
|
|
4681
|
+
@if (primaryActionLabel) {
|
|
4682
|
+
<button type="button" class="lokotro-btn-primary" (click)="onPrimaryClick()">
|
|
4683
|
+
{{ primaryActionLabel }}
|
|
4684
|
+
</button>
|
|
4685
|
+
}
|
|
4686
|
+
@if (secondaryActionLabel) {
|
|
4687
|
+
<button type="button" class="lokotro-btn-secondary" (click)="onSecondaryClick()">
|
|
4688
|
+
{{ secondaryActionLabel }}
|
|
4689
|
+
</button>
|
|
4690
|
+
}
|
|
4691
|
+
</div>
|
|
4692
|
+
}
|
|
4693
|
+
</div>
|
|
4694
|
+
`, styles: [".lokotro-result{display:flex;flex-direction:column;align-items:stretch;max-width:420px;margin:0 auto;padding:24px 16px;gap:20px}.lokotro-result-card{display:flex;flex-direction:column;align-items:center;text-align:center;padding:32px 24px;background:var(--lokotro-card, #FFFFFF);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:20px;box-shadow:0 1px 3px var(--lokotro-shadow-light, rgba(15, 23, 42, .04)),0 8px 24px var(--lokotro-shadow-light, rgba(15, 23, 42, .04))}.lokotro-status-badge{width:72px;height:72px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:24px;animation:lokotro-pop .5s cubic-bezier(.34,1.56,.64,1)}.lokotro-status-badge svg{width:36px;height:36px;color:#fff}.lokotro-status-success{background:var(--lokotro-success, #16A34A);box-shadow:0 0 0 8px #16a34a1f,0 8px 20px #16a34a40}.lokotro-status-error{background:var(--lokotro-error, #DC2626);box-shadow:0 0 0 8px #dc26261f,0 8px 20px #dc262640}.lokotro-status-warning{background:var(--lokotro-warning, #D97706);box-shadow:0 0 0 8px #d977061f,0 8px 20px #d9770640}.lokotro-status-info{background:var(--lokotro-info, #2563EB);box-shadow:0 0 0 8px #2563eb1f,0 8px 20px #2563eb40}@keyframes lokotro-pop{0%{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.lokotro-amount-focal{display:flex;flex-direction:column;align-items:center;gap:4px;margin-bottom:16px}.lokotro-amount-currency{font-size:12px;font-weight:600;letter-spacing:.08em;color:var(--lokotro-text-secondary, #64748B)}.lokotro-amount-value{font-size:40px;font-weight:700;line-height:1;color:var(--lokotro-text-primary, #0F172A);font-variant-numeric:tabular-nums}.lokotro-result-title{font-size:20px;font-weight:600;margin:0 0 6px;color:var(--lokotro-text-primary, #0F172A)}.lokotro-result-message{font-size:14px;color:var(--lokotro-text-secondary, #64748B);margin:0;line-height:1.5;max-width:320px}.lokotro-result-details{width:100%;background:var(--lokotro-surface, #F7F8FA);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:12px;padding:12px 16px;margin-top:24px}.lokotro-detail-row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:6px 0}.lokotro-detail-row:not(:last-child){border-bottom:1px solid var(--lokotro-divider, #F1F5F9)}.lokotro-detail-label{font-size:13px;color:var(--lokotro-text-secondary, #64748B)}.lokotro-detail-value{font-size:13px;font-weight:600;color:var(--lokotro-text-primary, #0F172A);text-align:right;overflow:hidden;text-overflow:ellipsis;max-width:200px;white-space:nowrap}.lokotro-mono{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Monaco,monospace;font-size:12px}.lokotro-result-actions{display:flex;flex-direction:column;gap:10px;width:100%}.lokotro-btn-primary,.lokotro-btn-secondary{width:100%;padding:14px 24px;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;transition:all .18s ease;font-family:inherit}.lokotro-btn-primary{background:var(--lokotro-primary, #0F172A);color:#fff;border:none}.lokotro-btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 16px var(--lokotro-shadow-medium, rgba(15, 23, 42, .12))}.lokotro-btn-primary:active{transform:translateY(0)}.lokotro-btn-secondary{background:transparent;color:var(--lokotro-text-primary, #0F172A);border:1.5px solid var(--lokotro-border, #E2E8F0)}.lokotro-btn-secondary:hover{border-color:var(--lokotro-text-secondary, #64748B);background:var(--lokotro-surface, #F7F8FA)}.lokotro-countdown-indicator{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;background:var(--lokotro-surface, #F7F8FA);border:1px solid var(--lokotro-border, #E2E8F0);border-radius:999px;cursor:pointer;margin-top:20px;transition:all .18s ease;color:var(--lokotro-text-secondary, #64748B);font-family:inherit}.lokotro-countdown-indicator:hover{background:var(--lokotro-card, #FFFFFF);border-color:var(--lokotro-text-tertiary, #64748B)}.lokotro-countdown-circle{width:14px;height:14px;transform:rotate(-90deg)}.lokotro-countdown-bg{fill:none;stroke:var(--lokotro-border, #E2E8F0);stroke-width:4}.lokotro-countdown-fill{fill:none;stroke-width:4;stroke-linecap:round;transition:stroke-dasharray .3s ease}.lokotro-countdown-success{stroke:var(--lokotro-success, #16A34A)}.lokotro-countdown-error{stroke:var(--lokotro-error, #DC2626)}.lokotro-countdown-warning{stroke:var(--lokotro-warning, #D97706)}.lokotro-countdown-info{stroke:var(--lokotro-info, #2563EB)}.lokotro-countdown-text{font-size:12px;font-weight:500}.lokotro-countdown-cancel{color:inherit;opacity:.7}\n"] }]
|
|
4695
|
+
}], ctorParameters: () => [{ type: LokotroLocalizationService }], propDecorators: { type: [{
|
|
4696
|
+
type: Input
|
|
4697
|
+
}], title: [{
|
|
4698
|
+
type: Input
|
|
4699
|
+
}], message: [{
|
|
4700
|
+
type: Input
|
|
4701
|
+
}], amount: [{
|
|
4702
|
+
type: Input
|
|
4703
|
+
}], currency: [{
|
|
4704
|
+
type: Input
|
|
4705
|
+
}], transactionId: [{
|
|
4706
|
+
type: Input
|
|
4707
|
+
}], primaryActionLabel: [{
|
|
4708
|
+
type: Input
|
|
4709
|
+
}], secondaryActionLabel: [{
|
|
4710
|
+
type: Input
|
|
4711
|
+
}], autoRedirectSeconds: [{
|
|
4712
|
+
type: Input
|
|
4713
|
+
}], primaryAction: [{
|
|
4714
|
+
type: Output
|
|
4715
|
+
}], secondaryAction: [{
|
|
4716
|
+
type: Output
|
|
4717
|
+
}], autoRedirect: [{
|
|
4718
|
+
type: Output
|
|
4719
|
+
}] } });
|
|
4720
|
+
|
|
4721
|
+
/**
|
|
4722
|
+
* Lokotro Pay - Loading Component
|
|
4723
|
+
*/
|
|
4724
|
+
class LokotroLoadingComponent {
|
|
4725
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroLoadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4726
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: LokotroLoadingComponent, isStandalone: true, selector: "lokotro-loading", inputs: { message: "message" }, ngImport: i0, template: `
|
|
4727
|
+
<div class="lokotro-loading">
|
|
4728
|
+
<div class="lokotro-loading-spinner">
|
|
4729
|
+
<div class="lokotro-pulse-ring"></div>
|
|
4730
|
+
<div class="lokotro-pulse-ring"></div>
|
|
4731
|
+
<div class="lokotro-pulse-dot"></div>
|
|
4732
|
+
</div>
|
|
4733
|
+
@if (message) {
|
|
4734
|
+
<p class="lokotro-loading-message">{{ message }}</p>
|
|
4735
|
+
}
|
|
4736
|
+
</div>
|
|
4737
|
+
`, isInline: true, styles: [".lokotro-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;padding:24px}.lokotro-loading-spinner{position:relative;width:60px;height:60px;margin-bottom:24px}.lokotro-pulse-ring{position:absolute;width:100%;height:100%;border:3px solid var(--lokotro-accent, #3BFBDA);border-radius:50%;opacity:0;animation:lokotro-pulse 2s cubic-bezier(.215,.61,.355,1) infinite}.lokotro-pulse-ring:nth-child(2){animation-delay:.5s}.lokotro-pulse-dot{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:16px;height:16px;background:var(--lokotro-accent, #3BFBDA);border-radius:50%;animation:lokotro-dot-pulse 2s ease-in-out infinite}@keyframes lokotro-pulse{0%{transform:scale(.5);opacity:.8}to{transform:scale(1.5);opacity:0}}@keyframes lokotro-dot-pulse{0%,to{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.2)}}.lokotro-loading-message{font-size:16px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0}\n"] }); }
|
|
4250
4738
|
}
|
|
4251
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type:
|
|
4252
|
-
type:
|
|
4253
|
-
args: [{
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4739
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroLoadingComponent, decorators: [{
|
|
4740
|
+
type: Component,
|
|
4741
|
+
args: [{ selector: 'lokotro-loading', standalone: true, imports: [], template: `
|
|
4742
|
+
<div class="lokotro-loading">
|
|
4743
|
+
<div class="lokotro-loading-spinner">
|
|
4744
|
+
<div class="lokotro-pulse-ring"></div>
|
|
4745
|
+
<div class="lokotro-pulse-ring"></div>
|
|
4746
|
+
<div class="lokotro-pulse-dot"></div>
|
|
4747
|
+
</div>
|
|
4748
|
+
@if (message) {
|
|
4749
|
+
<p class="lokotro-loading-message">{{ message }}</p>
|
|
4750
|
+
}
|
|
4751
|
+
</div>
|
|
4752
|
+
`, styles: [".lokotro-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;padding:24px}.lokotro-loading-spinner{position:relative;width:60px;height:60px;margin-bottom:24px}.lokotro-pulse-ring{position:absolute;width:100%;height:100%;border:3px solid var(--lokotro-accent, #3BFBDA);border-radius:50%;opacity:0;animation:lokotro-pulse 2s cubic-bezier(.215,.61,.355,1) infinite}.lokotro-pulse-ring:nth-child(2){animation-delay:.5s}.lokotro-pulse-dot{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:16px;height:16px;background:var(--lokotro-accent, #3BFBDA);border-radius:50%;animation:lokotro-dot-pulse 2s ease-in-out infinite}@keyframes lokotro-pulse{0%{transform:scale(.5);opacity:.8}to{transform:scale(1.5);opacity:0}}@keyframes lokotro-dot-pulse{0%,to{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.2)}}.lokotro-loading-message{font-size:16px;color:var(--lokotro-text-secondary, #D5D3B8);margin:0}\n"] }]
|
|
4753
|
+
}], propDecorators: { message: [{
|
|
4754
|
+
type: Input
|
|
4755
|
+
}] } });
|
|
4257
4756
|
|
|
4258
4757
|
/**
|
|
4259
4758
|
* Lokotro Pay - Checkout Component
|
|
@@ -4484,6 +4983,12 @@ class LokotroPayCheckoutComponent {
|
|
|
4484
4983
|
onCancel() {
|
|
4485
4984
|
this.closed.emit();
|
|
4486
4985
|
}
|
|
4986
|
+
onConfirmBankTransfer() {
|
|
4987
|
+
this.paymentService.confirmBankTransferSwitch();
|
|
4988
|
+
}
|
|
4989
|
+
onDeclineBankTransfer() {
|
|
4990
|
+
this.paymentService.declineBankTransferSwitch();
|
|
4991
|
+
}
|
|
4487
4992
|
onClose() {
|
|
4488
4993
|
this.closed.emit();
|
|
4489
4994
|
}
|
|
@@ -4600,7 +5105,34 @@ class LokotroPayCheckoutComponent {
|
|
|
4600
5105
|
(cancel)="onCancel()">
|
|
4601
5106
|
</lokotro-payment-form>
|
|
4602
5107
|
}
|
|
4603
|
-
|
|
5108
|
+
|
|
5109
|
+
<!-- Bank Transfer Form Screen (town/bank/account selection) -->
|
|
5110
|
+
@if (currentScreen === 'bankTransferFormScreen') {
|
|
5111
|
+
<lokotro-bank-transfer-form
|
|
5112
|
+
[showUserInfoForm]="state?.paymentInfo?.showUserInfoForm || false"
|
|
5113
|
+
(formSubmitted)="onFormSubmitted($event)"
|
|
5114
|
+
(cancel)="onCancel()">
|
|
5115
|
+
</lokotro-bank-transfer-form>
|
|
5116
|
+
}
|
|
5117
|
+
|
|
5118
|
+
<!-- Bank Transfer Switch Offer (amount out of range for chosen method) -->
|
|
5119
|
+
@if (currentScreen === 'bankTransferOfferScreen') {
|
|
5120
|
+
<div style="padding:24px;text-align:center;display:flex;flex-direction:column;gap:16px;">
|
|
5121
|
+
<h3 style="margin:0;font-size:18px;font-weight:600;">{{ localization.translate('bankTransferSwitchTitle') }}</h3>
|
|
5122
|
+
<p style="margin:0;opacity:0.75;">{{ localization.translate('bankTransferSwitchMessage') }}</p>
|
|
5123
|
+
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;">
|
|
5124
|
+
<button type="button" (click)="onDeclineBankTransfer()"
|
|
5125
|
+
style="padding:12px 20px;border-radius:10px;border:1px solid rgba(128,128,128,0.4);background:transparent;color:inherit;cursor:pointer;">
|
|
5126
|
+
{{ localization.translate('bankTransferSwitchDecline') }}
|
|
5127
|
+
</button>
|
|
5128
|
+
<button type="button" (click)="onConfirmBankTransfer()"
|
|
5129
|
+
style="padding:12px 20px;border-radius:10px;border:none;background:var(--lokotro-primary,#0891b2);color:#fff;cursor:pointer;font-weight:600;">
|
|
5130
|
+
{{ localization.translate('bankTransferSwitchConfirm') }}
|
|
5131
|
+
</button>
|
|
5132
|
+
</div>
|
|
5133
|
+
</div>
|
|
5134
|
+
}
|
|
5135
|
+
|
|
4604
5136
|
<!-- OTP Verification Screen -->
|
|
4605
5137
|
@if (currentScreen === 'ewalletOtpScreen') {
|
|
4606
5138
|
<lokotro-otp-verification
|
|
@@ -4673,13 +5205,14 @@ class LokotroPayCheckoutComponent {
|
|
|
4673
5205
|
}
|
|
4674
5206
|
</div>
|
|
4675
5207
|
</div>
|
|
4676
|
-
`, isInline: true, styles: [".lokotro-checkout{display:flex;flex-direction:column;min-height:100%;background:var(--lokotro-background, #FFFFFF);color:var(--lokotro-text-primary, #0F172A);font-family:var(--lokotro-font-family, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif)}.lokotro-checkout-header{display:flex;align-items:center;justify-content:space-between;padding:16px;background:var(--lokotro-background, #FFFFFF);border-bottom:1px solid var(--lokotro-border, #E2E8F0)}.lokotro-title{flex:1;text-align:center;font-size:18px;font-weight:600;margin:0;color:var(--lokotro-text-primary, #0F172A)}.lokotro-back-btn,.lokotro-close-btn{background:none;border:none;padding:8px;cursor:pointer;color:var(--lokotro-text-secondary, #475569);border-radius:8px;transition:background-color .2s,color .2s}.lokotro-back-btn:hover,.lokotro-close-btn:hover{background:var(--lokotro-surface, #F7F8FA);color:var(--lokotro-text-primary, #0F172A)}.lokotro-checkout-content{flex:1;padding:16px;overflow-y:auto}.lokotro-dark{--lokotro-background: #0F172A;--lokotro-surface: #1E293B;--lokotro-card: #1E293B;--lokotro-text-primary: #F8FAFC;--lokotro-text-secondary: #CBD5E1;--lokotro-border: #334155}\n"], dependencies: [{ kind: "component", type: LokotroPaymentMethodSelectionComponent, selector: "lokotro-payment-method-selection", inputs: ["paymentMethods", "selectedMethod"], outputs: ["methodSelected"] }, { kind: "component", type: LokotroPaymentFormComponent, selector: "lokotro-payment-form", inputs: ["channel", "transactionId", "showUserInfoForm"], outputs: ["formSubmitted", "cancel"] }, { kind: "component", type: LokotroOtpVerificationComponent, selector: "lokotro-otp-verification", inputs: ["transactionId", "otpDestination", "otpLength"], outputs: ["otpVerified", "resendOtp", "cancel"] }, { kind: "component", type: LokotroProcessingComponent, selector: "lokotro-processing", inputs: ["type", "message"] }, { kind: "component", type: LokotroResultComponent, selector: "lokotro-result", inputs: ["type", "title", "message", "amount", "currency", "transactionId", "primaryActionLabel", "secondaryActionLabel", "autoRedirectSeconds"], outputs: ["primaryAction", "secondaryAction", "autoRedirect"] }, { kind: "component", type: LokotroLoadingComponent, selector: "lokotro-loading", inputs: ["message"] }] }); }
|
|
5208
|
+
`, isInline: true, styles: [".lokotro-checkout{display:flex;flex-direction:column;min-height:100%;background:var(--lokotro-background, #FFFFFF);color:var(--lokotro-text-primary, #0F172A);font-family:var(--lokotro-font-family, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif)}.lokotro-checkout-header{display:flex;align-items:center;justify-content:space-between;padding:16px;background:var(--lokotro-background, #FFFFFF);border-bottom:1px solid var(--lokotro-border, #E2E8F0)}.lokotro-title{flex:1;text-align:center;font-size:18px;font-weight:600;margin:0;color:var(--lokotro-text-primary, #0F172A)}.lokotro-back-btn,.lokotro-close-btn{background:none;border:none;padding:8px;cursor:pointer;color:var(--lokotro-text-secondary, #475569);border-radius:8px;transition:background-color .2s,color .2s}.lokotro-back-btn:hover,.lokotro-close-btn:hover{background:var(--lokotro-surface, #F7F8FA);color:var(--lokotro-text-primary, #0F172A)}.lokotro-checkout-content{flex:1;padding:16px;overflow-y:auto}.lokotro-dark{--lokotro-background: #0F172A;--lokotro-surface: #1E293B;--lokotro-card: #1E293B;--lokotro-text-primary: #F8FAFC;--lokotro-text-secondary: #CBD5E1;--lokotro-border: #334155}\n"], dependencies: [{ kind: "component", type: LokotroPaymentMethodSelectionComponent, selector: "lokotro-payment-method-selection", inputs: ["paymentMethods", "selectedMethod"], outputs: ["methodSelected"] }, { kind: "component", type: LokotroPaymentFormComponent, selector: "lokotro-payment-form", inputs: ["channel", "transactionId", "showUserInfoForm"], outputs: ["formSubmitted", "cancel"] }, { kind: "component", type: LokotroBankTransferFormComponent, selector: "lokotro-bank-transfer-form", inputs: ["showUserInfoForm"], outputs: ["formSubmitted", "cancel"] }, { kind: "component", type: LokotroOtpVerificationComponent, selector: "lokotro-otp-verification", inputs: ["transactionId", "otpDestination", "otpLength"], outputs: ["otpVerified", "resendOtp", "cancel"] }, { kind: "component", type: LokotroProcessingComponent, selector: "lokotro-processing", inputs: ["type", "message"] }, { kind: "component", type: LokotroResultComponent, selector: "lokotro-result", inputs: ["type", "title", "message", "amount", "currency", "transactionId", "primaryActionLabel", "secondaryActionLabel", "autoRedirectSeconds"], outputs: ["primaryAction", "secondaryAction", "autoRedirect"] }, { kind: "component", type: LokotroLoadingComponent, selector: "lokotro-loading", inputs: ["message"] }] }); }
|
|
4677
5209
|
}
|
|
4678
5210
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LokotroPayCheckoutComponent, decorators: [{
|
|
4679
5211
|
type: Component,
|
|
4680
5212
|
args: [{ selector: 'lokotro-pay-checkout', standalone: true, imports: [
|
|
4681
5213
|
LokotroPaymentMethodSelectionComponent,
|
|
4682
5214
|
LokotroPaymentFormComponent,
|
|
5215
|
+
LokotroBankTransferFormComponent,
|
|
4683
5216
|
LokotroOtpVerificationComponent,
|
|
4684
5217
|
LokotroProcessingComponent,
|
|
4685
5218
|
LokotroResultComponent,
|
|
@@ -4734,7 +5267,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
4734
5267
|
(cancel)="onCancel()">
|
|
4735
5268
|
</lokotro-payment-form>
|
|
4736
5269
|
}
|
|
4737
|
-
|
|
5270
|
+
|
|
5271
|
+
<!-- Bank Transfer Form Screen (town/bank/account selection) -->
|
|
5272
|
+
@if (currentScreen === 'bankTransferFormScreen') {
|
|
5273
|
+
<lokotro-bank-transfer-form
|
|
5274
|
+
[showUserInfoForm]="state?.paymentInfo?.showUserInfoForm || false"
|
|
5275
|
+
(formSubmitted)="onFormSubmitted($event)"
|
|
5276
|
+
(cancel)="onCancel()">
|
|
5277
|
+
</lokotro-bank-transfer-form>
|
|
5278
|
+
}
|
|
5279
|
+
|
|
5280
|
+
<!-- Bank Transfer Switch Offer (amount out of range for chosen method) -->
|
|
5281
|
+
@if (currentScreen === 'bankTransferOfferScreen') {
|
|
5282
|
+
<div style="padding:24px;text-align:center;display:flex;flex-direction:column;gap:16px;">
|
|
5283
|
+
<h3 style="margin:0;font-size:18px;font-weight:600;">{{ localization.translate('bankTransferSwitchTitle') }}</h3>
|
|
5284
|
+
<p style="margin:0;opacity:0.75;">{{ localization.translate('bankTransferSwitchMessage') }}</p>
|
|
5285
|
+
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;">
|
|
5286
|
+
<button type="button" (click)="onDeclineBankTransfer()"
|
|
5287
|
+
style="padding:12px 20px;border-radius:10px;border:1px solid rgba(128,128,128,0.4);background:transparent;color:inherit;cursor:pointer;">
|
|
5288
|
+
{{ localization.translate('bankTransferSwitchDecline') }}
|
|
5289
|
+
</button>
|
|
5290
|
+
<button type="button" (click)="onConfirmBankTransfer()"
|
|
5291
|
+
style="padding:12px 20px;border-radius:10px;border:none;background:var(--lokotro-primary,#0891b2);color:#fff;cursor:pointer;font-weight:600;">
|
|
5292
|
+
{{ localization.translate('bankTransferSwitchConfirm') }}
|
|
5293
|
+
</button>
|
|
5294
|
+
</div>
|
|
5295
|
+
</div>
|
|
5296
|
+
}
|
|
5297
|
+
|
|
4738
5298
|
<!-- OTP Verification Screen -->
|
|
4739
5299
|
@if (currentScreen === 'ewalletOtpScreen') {
|
|
4740
5300
|
<lokotro-otp-verification
|
|
@@ -5632,5 +6192,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
5632
6192
|
* Generated bundle index. Do not edit.
|
|
5633
6193
|
*/
|
|
5634
6194
|
|
|
5635
|
-
export { LOKOTRO_ENV_CONFIG, LOKOTRO_PAY_CONFIG, LOKOTRO_PAY_FEATURE_FLAGS, LokotroHttpClientService, LokotroLoadingComponent, LokotroLocalizationService, LokotroOtpVerificationComponent, LokotroPayApiResponseCode, LokotroPayApiResponseCodeInfo, LokotroPayChannel, LokotroPayChannelInfo, LokotroPayCheckoutComponent, LokotroPayColors, LokotroPayEnv, LokotroPayFillingInfo, LokotroPayLanguage, LokotroPayLanguageInfo, LokotroPayModule, LokotroPayResultScreen, LokotroPayScreenNavigation, LokotroPayScreenNavigationInfo, LokotroPaymentFormComponent, LokotroPaymentMethodSelectionComponent, LokotroPaymentService, LokotroPaymentStatus, LokotroPaymentStatusComponent, LokotroProcessingComponent, LokotroResultComponent, visiblePaymentMethods };
|
|
6195
|
+
export { LOKOTRO_ENV_CONFIG, LOKOTRO_PAY_CONFIG, LOKOTRO_PAY_FEATURE_FLAGS, LokotroBankTransferFormComponent, LokotroHttpClientService, LokotroLoadingComponent, LokotroLocalizationService, LokotroOtpVerificationComponent, LokotroPayApiResponseCode, LokotroPayApiResponseCodeInfo, LokotroPayChannel, LokotroPayChannelInfo, LokotroPayCheckoutComponent, LokotroPayColors, LokotroPayEnv, LokotroPayFillingInfo, LokotroPayLanguage, LokotroPayLanguageInfo, LokotroPayModule, LokotroPayResultScreen, LokotroPayScreenNavigation, LokotroPayScreenNavigationInfo, LokotroPaymentFormComponent, LokotroPaymentMethodSelectionComponent, LokotroPaymentService, LokotroPaymentStatus, LokotroPaymentStatusComponent, LokotroProcessingComponent, LokotroResultComponent, visiblePaymentMethods };
|
|
5636
6196
|
//# sourceMappingURL=bloonio-lokotro-pay.mjs.map
|