@voxepay/checkout 0.3.11 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api/client.ts CHANGED
@@ -16,6 +16,17 @@ export interface InitiatePaymentRequest {
16
16
  paymentMethod: 'CARD' | 'BANK_TRANSFER';
17
17
  authData: string;
18
18
  narration?: string;
19
+ /** Virtual account validity in minutes (5-1440). Only for BANK_TRANSFER. Default: 30 */
20
+ durationMinutes?: number;
21
+ }
22
+
23
+ export interface VirtualAccountResponse {
24
+ account_number: string;
25
+ account_name: string;
26
+ bank_name: string;
27
+ bank_code: string;
28
+ expires_at: string;
29
+ status: string;
19
30
  }
20
31
 
21
32
  export interface InitiatePaymentResponse {
@@ -25,6 +36,33 @@ export interface InitiatePaymentResponse {
25
36
  status: string;
26
37
  message: string;
27
38
  eciFlag?: string;
39
+ otpRequired?: boolean;
40
+ virtualAccount?: VirtualAccountResponse;
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export type DVAPaymentStatus =
45
+ | 'INITIATED'
46
+ | 'PENDING_PAYMENT'
47
+ | 'PAYMENT_RECEIVED'
48
+ | 'COMPLETED'
49
+ | 'PARTIAL_PAYMENT'
50
+ | 'OVERPAID'
51
+ | 'EXPIRED'
52
+ | 'FAILED'
53
+ | 'CANCELLED';
54
+
55
+ export interface PaymentStatusResponse {
56
+ id: string;
57
+ transactionRef: string;
58
+ status: DVAPaymentStatus;
59
+ amount: number;
60
+ paymentMethod: string;
61
+ completedAt?: string;
62
+ virtualAccount?: {
63
+ account_number: string;
64
+ status: string;
65
+ };
28
66
  [key: string]: unknown;
29
67
  }
30
68
 
@@ -78,7 +116,32 @@ export class VoxePayApiClient {
78
116
  if (!response.ok) {
79
117
  const errorMessage = data?.message || data?.error || `Request failed with status ${response.status}`;
80
118
  const error: any = new Error(errorMessage);
81
- error.code = data?.code || `HTTP_${response.status}`;
119
+ error.code = data?.errorCode || data?.code || `HTTP_${response.status}`;
120
+ error.status = response.status;
121
+ error.data = data;
122
+ throw error;
123
+ }
124
+
125
+ return data as T;
126
+ }
127
+
128
+ private async get<T>(endpoint: string): Promise<T> {
129
+ const url = `${this.baseUrl}${endpoint}`;
130
+
131
+ const response = await fetch(url, {
132
+ method: 'GET',
133
+ headers: {
134
+ 'X-API-Key': this.apiKey,
135
+ 'Accept': 'application/json',
136
+ },
137
+ });
138
+
139
+ const data = await response.json();
140
+
141
+ if (!response.ok) {
142
+ const errorMessage = data?.message || data?.error || `Request failed with status ${response.status}`;
143
+ const error: any = new Error(errorMessage);
144
+ error.code = data?.errorCode || data?.code || `HTTP_${response.status}`;
82
145
  error.status = response.status;
83
146
  error.data = data;
84
147
  throw error;
@@ -88,10 +151,18 @@ export class VoxePayApiClient {
88
151
  }
89
152
 
90
153
  /**
91
- * Initiate a card payment
154
+ * Initiate a payment (card or bank transfer)
92
155
  */
93
156
  async initiatePayment(data: InitiatePaymentRequest): Promise<InitiatePaymentResponse> {
94
- return this.request<InitiatePaymentResponse>('/api/v1/payments/initiate', data);
157
+ const response = await this.request<{ success?: boolean; data?: InitiatePaymentResponse } & InitiatePaymentResponse>(
158
+ '/api/v1/payments/initiate',
159
+ data
160
+ );
161
+ // Handle both wrapped { success, data: {...} } and flat response formats
162
+ if (response.data && typeof response.data === 'object' && 'transactionRef' in response.data) {
163
+ return response.data;
164
+ }
165
+ return response;
95
166
  }
96
167
 
97
168
  /**
@@ -107,4 +178,14 @@ export class VoxePayApiClient {
107
178
  async resendOTP(data: ResendOTPRequest): Promise<void> {
108
179
  await this.request<unknown>('/api/v1/payments/resend-otp', data);
109
180
  }
181
+
182
+ /**
183
+ * Get payment status by transaction reference (used for polling DVA payments)
184
+ */
185
+ async getPaymentStatus(transactionRef: string): Promise<PaymentStatusResponse> {
186
+ const response = await this.get<{ success: boolean; data: PaymentStatusResponse }>(
187
+ `/api/v1/payments/${encodeURIComponent(transactionRef)}`
188
+ );
189
+ return response.data;
190
+ }
110
191
  }
@@ -150,6 +150,7 @@ export class VoxePayModal {
150
150
  */
151
151
  close(): void {
152
152
  this.stopTransferTimer();
153
+ this.stopStatusPolling();
153
154
  this.overlay?.classList.remove('voxepay-visible');
154
155
 
155
156
  setTimeout(() => {
@@ -762,24 +763,58 @@ export class VoxePayModal {
762
763
  }
763
764
 
764
765
  /**
765
- * Load bank transfer details (from callback or generate mock)
766
+ * Load bank transfer details (from callback or via API)
766
767
  */
767
768
  private async loadBankTransferDetails(): Promise<void> {
768
769
  try {
769
770
  let details: BankTransferDetails;
770
771
 
771
772
  if (this.options.onBankTransferRequested) {
773
+ // Use merchant-provided callback
772
774
  details = await this.options.onBankTransferRequested();
773
- } else {
774
- // Mock/default bank details for testing
775
- await new Promise(resolve => setTimeout(resolve, 1500));
775
+ } else if (this.apiClient && this.options._sdkConfig) {
776
+ // Call real DVA API
777
+ const transactionRef = `VP-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
778
+
779
+ const response = await this.apiClient.initiatePayment({
780
+ organizationId: this.options._sdkConfig.organizationId,
781
+ transactionRef,
782
+ customerId: this.options.customerEmail || '',
783
+ customerEmail: this.options.customerEmail,
784
+ customerPhone: this.options.customerPhone,
785
+ amount: this.options.amount / 100, // Convert from kobo/cents to main unit
786
+ currency: this.options.currency,
787
+ paymentMethod: 'BANK_TRANSFER',
788
+ authData: btoa(JSON.stringify({ source: 'web', method: 'bank_transfer' })),
789
+ narration: this.options.description,
790
+ durationMinutes: 30,
791
+ });
792
+
793
+ // Store transaction ref for status polling
794
+ // API may return 'id' (DVA) or 'paymentId' (card) depending on payment method
795
+ this.state.transactionRef = response.transactionRef || transactionRef;
796
+ this.state.paymentId = response.paymentId || (response as any).id || null;
797
+
798
+ const va = response.virtualAccount;
799
+ if (!va) {
800
+ throw { code: 'MISSING_VIRTUAL_ACCOUNT', message: 'No virtual account returned from server.' };
801
+ }
802
+
803
+ // Compute expiresIn from the ISO timestamp
804
+ const expiresAtDate = new Date(va.expires_at);
805
+ const nowMs = Date.now();
806
+ const expiresInSeconds = Math.max(0, Math.floor((expiresAtDate.getTime() - nowMs) / 1000));
807
+
776
808
  details = {
777
- accountNumber: '0123456789',
778
- bankName: 'VoxePay Bank',
779
- accountName: 'VoxePay Collections',
780
- reference: `VP-${Date.now().toString(36).toUpperCase()}`,
781
- expiresIn: 1800, // 30 minutes
809
+ accountNumber: va.account_number,
810
+ bankName: va.bank_name,
811
+ accountName: va.account_name,
812
+ reference: this.state.transactionRef!,
813
+ expiresIn: expiresInSeconds,
814
+ expiresAt: va.expires_at,
782
815
  };
816
+ } else {
817
+ throw { code: 'SDK_ERROR', message: 'SDK not properly initialized. Missing API key or organization ID.' };
783
818
  }
784
819
 
785
820
  this.state.bankTransferDetails = details;
@@ -792,15 +827,62 @@ export class VoxePayModal {
792
827
  this.attachBankTransferListeners();
793
828
  this.startTransferTimer();
794
829
  }
795
- } catch (error) {
830
+ } catch (error: any) {
831
+ const errorCode = error?.code || 'BANK_TRANSFER_INIT_FAILED';
832
+ const errorMessage = error?.message || 'Could not generate bank transfer details. Please try again.';
833
+
834
+ // Show error in the loading area
835
+ const formContainer = this.container?.querySelector('#voxepay-form-container');
836
+ if (formContainer) {
837
+ formContainer.innerHTML = `
838
+ <div class="voxepay-transfer-view">
839
+ <div style="text-align: center; padding: 40px 16px;">
840
+ <div style="font-size: 2.5rem; margin-bottom: 16px;">⚠️</div>
841
+ <h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">${this.getDVAErrorTitle(errorCode)}</h3>
842
+ <p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 24px;">${errorMessage}</p>
843
+ <button class="voxepay-submit-btn" id="voxepay-retry-transfer"><span>Try Again</span></button>
844
+ </div>
845
+ </div>
846
+ `;
847
+ const retryBtn = formContainer.querySelector('#voxepay-retry-transfer');
848
+ retryBtn?.addEventListener('click', () => {
849
+ formContainer.innerHTML = `
850
+ <div class="voxepay-transfer-view">
851
+ <div class="voxepay-transfer-loading">
852
+ <div class="voxepay-spinner"></div>
853
+ <p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
854
+ </div>
855
+ </div>
856
+ `;
857
+ this.loadBankTransferDetails();
858
+ });
859
+ }
860
+
796
861
  this.options.onError({
797
- code: 'BANK_TRANSFER_INIT_FAILED',
798
- message: 'Could not generate bank transfer details. Please try again.',
862
+ code: errorCode,
863
+ message: errorMessage,
799
864
  recoverable: true,
865
+ details: error?.data,
800
866
  });
801
867
  }
802
868
  }
803
869
 
870
+ /**
871
+ * Get user-friendly error title for DVA error codes
872
+ */
873
+ private getDVAErrorTitle(code: string): string {
874
+ const titles: Record<string, string> = {
875
+ 'ACCOUNT_CREATION_FAILED': 'Account Generation Failed',
876
+ 'ACCOUNT_EXPIRED': 'Account Expired',
877
+ 'INVALID_AMOUNT': 'Invalid Amount',
878
+ 'UNAUTHORIZED': 'Authentication Failed',
879
+ 'ORGANIZATION_NOT_FOUND': 'Configuration Error',
880
+ 'MISSING_VIRTUAL_ACCOUNT': 'Account Generation Failed',
881
+ 'SDK_ERROR': 'Configuration Error',
882
+ };
883
+ return titles[code] || 'Something Went Wrong';
884
+ }
885
+
804
886
  /**
805
887
  * Copy text to clipboard with visual feedback
806
888
  */
@@ -829,8 +911,13 @@ export class VoxePayModal {
829
911
  }
830
912
  }
831
913
 
914
+ /** Polling interval ID for DVA status checks */
915
+ private statusPollingInterval: number | null = null;
916
+ /** Polling timeout (auto-stop after 30 minutes) */
917
+ private statusPollingTimeout: number | null = null;
918
+
832
919
  /**
833
- * Handle "I've sent the money" confirmation
920
+ * Handle "I've sent the money" confirmation — starts polling for payment status
834
921
  */
835
922
  private async handleTransferConfirm(): Promise<void> {
836
923
  const confirmBtn = this.container?.querySelector('#voxepay-transfer-confirm') as HTMLButtonElement;
@@ -839,14 +926,11 @@ export class VoxePayModal {
839
926
  confirmBtn.innerHTML = '<div class="voxepay-spinner"></div><span>Confirming transfer...</span>';
840
927
  }
841
928
 
842
- this.stopTransferTimer();
843
-
844
- try {
845
- // Simulate transfer verification
846
- await new Promise(resolve => setTimeout(resolve, 3000));
847
-
929
+ // If no API client or no transaction ref, fall back to pending result
930
+ if (!this.apiClient || !this.state.transactionRef) {
931
+ this.stopTransferTimer();
848
932
  const result: PaymentResult = {
849
- id: `pay_transfer_${Date.now()}`,
933
+ id: this.state.paymentId || `pay_transfer_${Date.now()}`,
850
934
  status: 'pending',
851
935
  amount: this.options.amount,
852
936
  currency: this.options.currency,
@@ -854,20 +938,252 @@ export class VoxePayModal {
854
938
  reference: this.state.bankTransferDetails?.reference,
855
939
  paymentMethod: 'bank_transfer',
856
940
  };
857
-
858
941
  this.state.isSuccess = true;
859
942
  this.renderSuccessView();
860
943
  this.options.onSuccess(result);
861
- } catch (error) {
862
- if (confirmBtn) {
863
- confirmBtn.disabled = false;
864
- confirmBtn.innerHTML = "<span>I've sent the money</span>";
944
+ return;
945
+ }
946
+
947
+ // Show polling UI
948
+ this.renderPollingView();
949
+ this.startStatusPolling();
950
+ }
951
+
952
+ /**
953
+ * Render the "waiting for payment confirmation" polling view
954
+ */
955
+ private renderPollingView(): void {
956
+ const formContainer = this.container?.querySelector('#voxepay-form-container');
957
+ if (!formContainer) return;
958
+
959
+ const formattedAmount = formatAmount(this.options.amount, this.options.currency);
960
+
961
+ formContainer.innerHTML = `
962
+ <div class="voxepay-transfer-view">
963
+ <div style="text-align: center; padding: 32px 16px;">
964
+ <div class="voxepay-spinner" style="width: 40px; height: 40px; margin: 0 auto 20px; border-width: 3px;"></div>
965
+ <h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">Waiting for Payment</h3>
966
+ <p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 4px;">
967
+ We're checking for your transfer of <strong>${formattedAmount}</strong>
968
+ </p>
969
+ <p style="font-size: 0.813rem; color: var(--voxepay-text-subtle); margin-bottom: 24px;" id="voxepay-polling-status">
970
+ This usually takes a few seconds...
971
+ </p>
972
+ <button class="voxepay-submit-btn" id="voxepay-cancel-polling" style="background: var(--voxepay-surface); border: 1px solid var(--voxepay-border); color: var(--voxepay-text-muted); box-shadow: none;">
973
+ <span>Cancel</span>
974
+ </button>
975
+ </div>
976
+ </div>
977
+ `;
978
+
979
+ const cancelBtn = formContainer.querySelector('#voxepay-cancel-polling');
980
+ cancelBtn?.addEventListener('click', () => {
981
+ this.stopStatusPolling();
982
+ // Re-render bank transfer view so user can try again
983
+ if (this.state.bankTransferDetails) {
984
+ const amt = formatAmount(this.options.amount, this.options.currency);
985
+ formContainer.innerHTML = this.getBankTransferHTML(amt);
986
+ this.attachBankTransferListeners();
987
+ this.startTransferTimer();
865
988
  }
866
- const paymentError = error as PaymentError;
867
- this.options.onError(paymentError);
989
+ });
990
+ }
991
+
992
+ /**
993
+ * Start polling payment status every 5 seconds
994
+ */
995
+ private startStatusPolling(): void {
996
+ this.stopStatusPolling(); // clear any existing poll
997
+
998
+ const poll = async () => {
999
+ if (!this.apiClient || !this.state.transactionRef) return;
1000
+
1001
+ try {
1002
+ const statusData = await this.apiClient.getPaymentStatus(this.state.transactionRef);
1003
+ this.handlePollingStatusUpdate(statusData.status, statusData);
1004
+ } catch (error: any) {
1005
+ console.error('[VoxePay] Status polling error:', error);
1006
+ // Don't stop polling on transient errors — let it retry
1007
+ const statusEl = this.container?.querySelector('#voxepay-polling-status');
1008
+ if (statusEl) {
1009
+ statusEl.textContent = 'Still checking... Please wait.';
1010
+ }
1011
+ }
1012
+ };
1013
+
1014
+ // Initial check immediately
1015
+ poll();
1016
+
1017
+ // Then poll every 5 seconds
1018
+ this.statusPollingInterval = window.setInterval(poll, 5000);
1019
+
1020
+ // Auto-stop after 30 minutes
1021
+ this.statusPollingTimeout = window.setTimeout(() => {
1022
+ this.stopStatusPolling();
1023
+ this.handlePollingStatusUpdate('EXPIRED', null);
1024
+ }, 30 * 60 * 1000);
1025
+ }
1026
+
1027
+ /**
1028
+ * Stop payment status polling
1029
+ */
1030
+ private stopStatusPolling(): void {
1031
+ if (this.statusPollingInterval) {
1032
+ clearInterval(this.statusPollingInterval);
1033
+ this.statusPollingInterval = null;
1034
+ }
1035
+ if (this.statusPollingTimeout) {
1036
+ clearTimeout(this.statusPollingTimeout);
1037
+ this.statusPollingTimeout = null;
868
1038
  }
869
1039
  }
870
1040
 
1041
+ /**
1042
+ * Handle status updates from polling
1043
+ */
1044
+ private handlePollingStatusUpdate(status: string, data: any): void {
1045
+ const terminalStatuses = ['COMPLETED', 'FAILED', 'EXPIRED', 'CANCELLED'];
1046
+
1047
+ if (terminalStatuses.includes(status)) {
1048
+ this.stopStatusPolling();
1049
+ this.stopTransferTimer();
1050
+ }
1051
+
1052
+ switch (status) {
1053
+ case 'COMPLETED': {
1054
+ const result: PaymentResult = {
1055
+ id: data?.id || this.state.paymentId || `pay_transfer_${Date.now()}`,
1056
+ status: 'success',
1057
+ amount: this.options.amount,
1058
+ currency: this.options.currency,
1059
+ timestamp: data?.completedAt || new Date().toISOString(),
1060
+ reference: this.state.transactionRef || undefined,
1061
+ paymentMethod: 'bank_transfer',
1062
+ data: data || undefined,
1063
+ };
1064
+ this.state.isSuccess = true;
1065
+ this.renderSuccessView();
1066
+ this.options.onSuccess(result);
1067
+ break;
1068
+ }
1069
+
1070
+ case 'PAYMENT_RECEIVED': {
1071
+ // Almost done — update the polling UI message
1072
+ const statusEl = this.container?.querySelector('#voxepay-polling-status');
1073
+ if (statusEl) {
1074
+ statusEl.textContent = 'Payment received! Processing...';
1075
+ }
1076
+ break;
1077
+ }
1078
+
1079
+ case 'PARTIAL_PAYMENT': {
1080
+ this.stopStatusPolling();
1081
+ this.renderTransferErrorView(
1082
+ 'Partial Payment Received',
1083
+ 'The amount transferred is less than the required amount. Please transfer the remaining balance or contact support.'
1084
+ );
1085
+ break;
1086
+ }
1087
+
1088
+ case 'OVERPAID': {
1089
+ // Overpayment is still successful — show success but note the overpayment
1090
+ const result: PaymentResult = {
1091
+ id: data?.id || this.state.paymentId || `pay_transfer_${Date.now()}`,
1092
+ status: 'success',
1093
+ amount: this.options.amount,
1094
+ currency: this.options.currency,
1095
+ timestamp: data?.completedAt || new Date().toISOString(),
1096
+ reference: this.state.transactionRef || undefined,
1097
+ paymentMethod: 'bank_transfer',
1098
+ data: { ...data, overpaid: true },
1099
+ };
1100
+ this.state.isSuccess = true;
1101
+ this.renderSuccessView();
1102
+ this.options.onSuccess(result);
1103
+ break;
1104
+ }
1105
+
1106
+ case 'EXPIRED': {
1107
+ this.renderTransferErrorView(
1108
+ 'Payment Expired',
1109
+ 'The virtual account has expired. Please try again to generate a new account.'
1110
+ );
1111
+ this.options.onError({
1112
+ code: 'ACCOUNT_EXPIRED',
1113
+ message: 'Virtual account expired before payment was received.',
1114
+ recoverable: true,
1115
+ });
1116
+ break;
1117
+ }
1118
+
1119
+ case 'FAILED': {
1120
+ this.renderTransferErrorView(
1121
+ 'Payment Failed',
1122
+ data?.message || 'The payment could not be processed. Please try again.'
1123
+ );
1124
+ this.options.onError({
1125
+ code: 'PAYMENT_FAILED',
1126
+ message: data?.message || 'Payment failed.',
1127
+ recoverable: true,
1128
+ });
1129
+ break;
1130
+ }
1131
+
1132
+ case 'CANCELLED': {
1133
+ this.renderTransferErrorView(
1134
+ 'Payment Cancelled',
1135
+ 'This payment has been cancelled.'
1136
+ );
1137
+ this.options.onError({
1138
+ code: 'PAYMENT_CANCELLED',
1139
+ message: 'Payment was cancelled.',
1140
+ recoverable: false,
1141
+ });
1142
+ break;
1143
+ }
1144
+
1145
+ default:
1146
+ // INITIATED, PENDING_PAYMENT — keep polling
1147
+ break;
1148
+ }
1149
+ }
1150
+
1151
+ /**
1152
+ * Render error view for transfer issues with retry option
1153
+ */
1154
+ private renderTransferErrorView(title: string, message: string): void {
1155
+ const formContainer = this.container?.querySelector('#voxepay-form-container');
1156
+ if (!formContainer) return;
1157
+
1158
+ formContainer.innerHTML = `
1159
+ <div class="voxepay-transfer-view">
1160
+ <div style="text-align: center; padding: 40px 16px;">
1161
+ <div style="font-size: 2.5rem; margin-bottom: 16px;">❌</div>
1162
+ <h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">${title}</h3>
1163
+ <p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 24px;">${message}</p>
1164
+ <button class="voxepay-submit-btn" id="voxepay-retry-transfer"><span>Try Again</span></button>
1165
+ </div>
1166
+ </div>
1167
+ `;
1168
+
1169
+ const retryBtn = formContainer.querySelector('#voxepay-retry-transfer');
1170
+ retryBtn?.addEventListener('click', () => {
1171
+ // Reset state for new attempt
1172
+ this.state.bankTransferDetails = null;
1173
+ this.state.transactionRef = null;
1174
+ this.state.paymentId = null;
1175
+ formContainer.innerHTML = `
1176
+ <div class="voxepay-transfer-view">
1177
+ <div class="voxepay-transfer-loading">
1178
+ <div class="voxepay-spinner"></div>
1179
+ <p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
1180
+ </div>
1181
+ </div>
1182
+ `;
1183
+ this.loadBankTransferDetails();
1184
+ });
1185
+ }
1186
+
871
1187
  /**
872
1188
  * Start the transfer countdown timer
873
1189
  */
package/src/index.ts CHANGED
@@ -67,6 +67,9 @@ export type {
67
67
  ValidateOTPRequest,
68
68
  ValidateOTPResponse,
69
69
  ResendOTPRequest,
70
+ VirtualAccountResponse,
71
+ PaymentStatusResponse,
72
+ DVAPaymentStatus,
70
73
  } from './api/client';
71
74
 
72
75
  // Default export for convenience
package/src/types.ts CHANGED
@@ -49,6 +49,8 @@ export interface BankTransferDetails {
49
49
  reference: string;
50
50
  /** Expiry time in seconds (how long the account is valid for, e.g. 1800 = 30 minutes) */
51
51
  expiresIn: number;
52
+ /** ISO timestamp when the virtual account expires (from API response) */
53
+ expiresAt?: string;
52
54
  }
53
55
 
54
56
  /** Supported payment method types */
@@ -446,8 +446,8 @@ export const generateAuthData = async ({
446
446
  cvv,
447
447
  }: AuthDataParams): Promise<string> => {
448
448
  try {
449
- // Build the cipher text: version + "1Z" + pan + "Z" + pin + ...
450
- const authDataCipher = `${version}1Z${pan}Z${pin}Z${expiryDate}Z${cvv}`;
449
+ // Build the cipher text: version + "Z" + pan + "Z" + pin + ...
450
+ const authDataCipher = `${version}Z${pan}Z${pin}Z${expiryDate}Z${cvv}`;
451
451
  const messageBytes = hexToBytes(toHex(authDataCipher));
452
452
 
453
453
  // Parse RSA public key