@patalink/node-api-client 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -43,7 +43,7 @@ import { createClient } from '@patlink/node-api-client';
43
43
  const client = createClient({
44
44
  apiKey: 'pk_live_abc123...',
45
45
  encryptionKey: 'xK8j3Hd9Fq2Wm5R...',
46
- baseUrl: 'https://api.patalink.me'
46
+ baseUrl: 'https://patalink.me'
47
47
  });
48
48
  ```
49
49
 
@@ -105,7 +105,7 @@ interface ClientConfig {
105
105
 
106
106
  ### Getting Your Keys
107
107
 
108
- 1. Sign up at [dashboard.patalink.me](https://dashboard.patalink.me)
108
+ 1. Sign up at [patalink.me](https://patalink.me)
109
109
  2. Navigate to **Settings** → **API Keys**
110
110
  3. Click **Generate API Key** and **Generate Encryption Key**
111
111
  4. Copy both keys to your environment variables
@@ -115,7 +115,7 @@ interface ClientConfig {
115
115
  ```env
116
116
  PATALINK_API_KEY=pk_live_abc123def456...
117
117
  PATALINK_ENCRYPTION_KEY=xK8j3Hd9Fq2Wm5R...
118
- PATALINK_BASE_URL=https://api.patalink.me
118
+ PATALINK_BASE_URL=https://patalink.me
119
119
  ```
120
120
 
121
121
  ```javascript
@@ -528,7 +528,7 @@ This package is designed for **Node.js server-side** use only. For browser/clien
528
528
  </script>
529
529
 
530
530
  <payment-modal
531
- base-url="https://api.patalink.me"
531
+ base-url="https://patalink.me"
532
532
  api-key="your_api_key"
533
533
  encryption-key="your_encryption_key"
534
534
  ></payment-modal>
package/dist/index.d.mts CHANGED
@@ -5,6 +5,7 @@ interface PaymentRequest {
5
5
  phoneNumber?: string;
6
6
  customerName?: string;
7
7
  customerEmail?: string;
8
+ callbackUrl?: string;
8
9
  }
9
10
  interface PaymentResponse {
10
11
  transactionId: string;
@@ -51,6 +52,64 @@ declare class PataLinkClient {
51
52
  * Get transaction status
52
53
  */
53
54
  getTransactionStatus(transactionId: string): Promise<TransactionStatus>;
55
+ /**
56
+ * Poll for transaction completion
57
+ */
58
+ waitForCompletion(transactionId: string, options?: {
59
+ maxAttempts?: number;
60
+ interval?: number;
61
+ onProgress?: (attempt: number, status: string) => void;
62
+ }): Promise<TransactionStatus>;
63
+ /**
64
+ * List all transactions for this account
65
+ */
66
+ listTransactions(options?: {
67
+ limit?: number;
68
+ offset?: number;
69
+ status?: 'pending' | 'success' | 'failed';
70
+ }): Promise<{
71
+ transactions: TransactionStatus[];
72
+ total: number;
73
+ }>;
74
+ /**
75
+ * Rotate encryption key (generates new key, revokes old one after 24h)
76
+ */
77
+ rotateEncryptionKey(): Promise<{
78
+ newEncryptionKey: string;
79
+ message: string;
80
+ }>;
81
+ /**
82
+ * Get account information
83
+ */
84
+ getAccountInfo(): Promise<{
85
+ id: string;
86
+ email: string;
87
+ walletBalance: number;
88
+ pendingBalance: number;
89
+ availableBalance: number;
90
+ }>;
91
+ /**
92
+ * Initiate withdrawal
93
+ */
94
+ createWithdrawal(request: {
95
+ amount: number;
96
+ phoneNumber: string;
97
+ network: 'MTN' | 'Airtel';
98
+ }): Promise<{
99
+ withdrawalId: string;
100
+ status: string;
101
+ message: string;
102
+ }>;
103
+ /**
104
+ * List withdrawals
105
+ */
106
+ listWithdrawals(options?: {
107
+ limit?: number;
108
+ offset?: number;
109
+ }): Promise<{
110
+ withdrawals: any[];
111
+ total: number;
112
+ }>;
54
113
  }
55
114
  declare function createClient(config: ClientConfig): PataLinkClient;
56
115
 
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ interface PaymentRequest {
5
5
  phoneNumber?: string;
6
6
  customerName?: string;
7
7
  customerEmail?: string;
8
+ callbackUrl?: string;
8
9
  }
9
10
  interface PaymentResponse {
10
11
  transactionId: string;
@@ -51,6 +52,64 @@ declare class PataLinkClient {
51
52
  * Get transaction status
52
53
  */
53
54
  getTransactionStatus(transactionId: string): Promise<TransactionStatus>;
55
+ /**
56
+ * Poll for transaction completion
57
+ */
58
+ waitForCompletion(transactionId: string, options?: {
59
+ maxAttempts?: number;
60
+ interval?: number;
61
+ onProgress?: (attempt: number, status: string) => void;
62
+ }): Promise<TransactionStatus>;
63
+ /**
64
+ * List all transactions for this account
65
+ */
66
+ listTransactions(options?: {
67
+ limit?: number;
68
+ offset?: number;
69
+ status?: 'pending' | 'success' | 'failed';
70
+ }): Promise<{
71
+ transactions: TransactionStatus[];
72
+ total: number;
73
+ }>;
74
+ /**
75
+ * Rotate encryption key (generates new key, revokes old one after 24h)
76
+ */
77
+ rotateEncryptionKey(): Promise<{
78
+ newEncryptionKey: string;
79
+ message: string;
80
+ }>;
81
+ /**
82
+ * Get account information
83
+ */
84
+ getAccountInfo(): Promise<{
85
+ id: string;
86
+ email: string;
87
+ walletBalance: number;
88
+ pendingBalance: number;
89
+ availableBalance: number;
90
+ }>;
91
+ /**
92
+ * Initiate withdrawal
93
+ */
94
+ createWithdrawal(request: {
95
+ amount: number;
96
+ phoneNumber: string;
97
+ network: 'MTN' | 'Airtel';
98
+ }): Promise<{
99
+ withdrawalId: string;
100
+ status: string;
101
+ message: string;
102
+ }>;
103
+ /**
104
+ * List withdrawals
105
+ */
106
+ listWithdrawals(options?: {
107
+ limit?: number;
108
+ offset?: number;
109
+ }): Promise<{
110
+ withdrawals: any[];
111
+ total: number;
112
+ }>;
54
113
  }
55
114
  declare function createClient(config: ClientConfig): PataLinkClient;
56
115
 
package/dist/index.js CHANGED
@@ -50,7 +50,12 @@ var PataLinkClient = class {
50
50
  */
51
51
  encrypt(data) {
52
52
  const dataString = JSON.stringify(data);
53
- const keyWA = CryptoJS.enc.Base64.parse(this.encryptionKey);
53
+ let keyWA;
54
+ try {
55
+ keyWA = CryptoJS.enc.Base64.parse(this.encryptionKey);
56
+ } catch (error) {
57
+ keyWA = CryptoJS.enc.Utf8.parse(this.encryptionKey);
58
+ }
54
59
  const iv = CryptoJS.lib.WordArray.random(16);
55
60
  const ivHex = iv.toString(CryptoJS.enc.Hex);
56
61
  const ivWA = CryptoJS.enc.Hex.parse(ivHex);
@@ -106,6 +111,9 @@ var PataLinkClient = class {
106
111
  if (request.paymentMethod !== "PesaPal" && !request.phoneNumber) {
107
112
  throw new Error("phoneNumber is required for mobile money payments");
108
113
  }
114
+ if (request.paymentMethod === "PesaPal" && !request.callbackUrl) {
115
+ throw new Error("callbackUrl is required for PesaPal (card) payments");
116
+ }
109
117
  if (request.amount < 100) {
110
118
  throw new Error("Amount must be at least 100 RWF");
111
119
  }
@@ -120,6 +128,9 @@ var PataLinkClient = class {
120
128
  supporterName: request.customerName || "",
121
129
  supporterEmail: request.customerEmail || ""
122
130
  };
131
+ if (request.paymentMethod === "PesaPal" && request.callbackUrl) {
132
+ paymentData.callbackUrl = request.callbackUrl;
133
+ }
123
134
  const encryptedPayload = this.encrypt(paymentData);
124
135
  const response = await this.request("/api/pay", {
125
136
  method: "POST",
@@ -142,98 +153,77 @@ var PataLinkClient = class {
142
153
  /**
143
154
  * Poll for transaction completion
144
155
  */
145
- // async waitForCompletion(
146
- // transactionId: string,
147
- // options?: {
148
- // maxAttempts?: number;
149
- // interval?: number;
150
- // onProgress?: (attempt: number, status: string) => void;
151
- // }
152
- // ): Promise<TransactionStatus> {
153
- // const maxAttempts = options?.maxAttempts || 60;
154
- // const interval = options?.interval || 2000;
155
- // let attempts = 0;
156
- // while (attempts < maxAttempts) {
157
- // attempts++;
158
- // options?.onProgress?.(attempts, 'checking');
159
- // const status = await this.getTransactionStatus(transactionId);
160
- // if (status.status === 'success' || status.status === 'failed') {
161
- // return status;
162
- // }
163
- // // Wait before next attempt
164
- // await new Promise(resolve => setTimeout(resolve, interval));
165
- // }
166
- // throw new Error('Transaction timeout - still pending after maximum attempts');
167
- // }
156
+ async waitForCompletion(transactionId, options) {
157
+ const maxAttempts = options?.maxAttempts || 60;
158
+ const interval = options?.interval || 2e3;
159
+ let attempts = 0;
160
+ while (attempts < maxAttempts) {
161
+ attempts++;
162
+ options?.onProgress?.(attempts, "checking");
163
+ const status = await this.getTransactionStatus(transactionId);
164
+ if (status.status === "success" || status.status === "failed") {
165
+ return status;
166
+ }
167
+ await new Promise((resolve) => setTimeout(resolve, interval));
168
+ }
169
+ throw new Error("Transaction timeout - still pending after maximum attempts");
170
+ }
168
171
  /**
169
172
  * List all transactions for this account
170
173
  */
171
- // async listTransactions(options?: {
172
- // limit?: number;
173
- // offset?: number;
174
- // status?: 'pending' | 'success' | 'failed';
175
- // }): Promise<{ transactions: TransactionStatus[]; total: number }> {
176
- // const params = new URLSearchParams();
177
- // if (options?.limit) params.append('limit', options.limit.toString());
178
- // if (options?.offset) params.append('offset', options.offset.toString());
179
- // if (options?.status) params.append('status', options.status);
180
- // const query = params.toString();
181
- // const endpoint = `/v1/transactions${query ? `?${query}` : ''}`;
182
- // return this.request<{ transactions: TransactionStatus[]; total: number }>(endpoint, {
183
- // method: 'GET'
184
- // });
185
- // }
174
+ async listTransactions(options) {
175
+ const params = new URLSearchParams();
176
+ if (options?.limit) params.append("limit", options.limit.toString());
177
+ if (options?.offset) params.append("offset", options.offset.toString());
178
+ if (options?.status) params.append("status", options.status);
179
+ const query = params.toString();
180
+ const endpoint = `/api/transactions${query ? `?${query}` : ""}`;
181
+ return this.request(endpoint, {
182
+ method: "GET"
183
+ });
184
+ }
186
185
  /**
187
186
  * Rotate encryption key (generates new key, revokes old one after 24h)
188
187
  */
189
- // async rotateEncryptionKey(): Promise<{ newEncryptionKey: string; message: string }> {
190
- // return this.request<{ newEncryptionKey: string; message: string }>('/v1/keys/rotate', {
191
- // method: 'POST'
192
- // });
193
- // }
188
+ async rotateEncryptionKey() {
189
+ return this.request("/api/keys/rotate", {
190
+ method: "POST"
191
+ });
192
+ }
194
193
  /**
195
194
  * Get account information
196
195
  */
197
- // async getAccountInfo(): Promise<{
198
- // id: string;
199
- // email: string;
200
- // walletBalance: number;
201
- // pendingBalance: number;
202
- // availableBalance: number;
203
- // }> {
204
- // return this.request('/v1/account', { method: 'GET' });
205
- // }
196
+ async getAccountInfo() {
197
+ return this.request("/api/account", { method: "GET" });
198
+ }
206
199
  /**
207
200
  * Initiate withdrawal
208
201
  */
209
- // async createWithdrawal(request: {
210
- // amount: number;
211
- // phoneNumber: string;
212
- // network: 'MTN' | 'Airtel';
213
- // }): Promise<{ withdrawalId: string; status: string; message: string }> {
214
- // if (!request.amount) throw new Error('amount is required');
215
- // if (!request.phoneNumber) throw new Error('phoneNumber is required');
216
- // if (request.amount < 100) throw new Error('Minimum withdrawal is 100 RWF');
217
- // if (request.amount > 2000000) throw new Error('Maximum withdrawal is 2,000,000 RWF');
218
- // return this.request('/v1/withdrawals', {
219
- // method: 'POST',
220
- // body: JSON.stringify(request)
221
- // });
222
- // }
202
+ async createWithdrawal(request) {
203
+ if (!request.amount) throw new Error("amount is required");
204
+ if (!request.phoneNumber) throw new Error("phoneNumber is required");
205
+ if (request.amount < 100) throw new Error("Minimum withdrawal is 100 RWF");
206
+ if (request.amount > 2e6) throw new Error("Maximum withdrawal is 2,000,000 RWF");
207
+ const encryptedPayload = this.encrypt(request);
208
+ return this.request("/api/withdrawals", {
209
+ method: "POST",
210
+ headers: {
211
+ "x-encrypted": "true"
212
+ },
213
+ body: JSON.stringify(encryptedPayload)
214
+ });
215
+ }
223
216
  /**
224
217
  * List withdrawals
225
218
  */
226
- // async listWithdrawals(options?: {
227
- // limit?: number;
228
- // offset?: number;
229
- // }): Promise<{ withdrawals: any[]; total: number }> {
230
- // const params = new URLSearchParams();
231
- // if (options?.limit) params.append('limit', options.limit.toString());
232
- // if (options?.offset) params.append('offset', options.offset.toString());
233
- // const query = params.toString();
234
- // const endpoint = `/v1/withdrawals${query ? `?${query}` : ''}`;
235
- // return this.request(endpoint, { method: 'GET' });
236
- // }
219
+ async listWithdrawals(options) {
220
+ const params = new URLSearchParams();
221
+ if (options?.limit) params.append("limit", options.limit.toString());
222
+ if (options?.offset) params.append("offset", options.offset.toString());
223
+ const query = params.toString();
224
+ const endpoint = `/api/withdrawals${query ? `?${query}` : ""}`;
225
+ return this.request(endpoint, { method: "GET" });
226
+ }
237
227
  };
238
228
  function createClient(config) {
239
229
  return new PataLinkClient(config);
package/dist/index.mjs CHANGED
@@ -15,7 +15,12 @@ var PataLinkClient = class {
15
15
  */
16
16
  encrypt(data) {
17
17
  const dataString = JSON.stringify(data);
18
- const keyWA = CryptoJS.enc.Base64.parse(this.encryptionKey);
18
+ let keyWA;
19
+ try {
20
+ keyWA = CryptoJS.enc.Base64.parse(this.encryptionKey);
21
+ } catch (error) {
22
+ keyWA = CryptoJS.enc.Utf8.parse(this.encryptionKey);
23
+ }
19
24
  const iv = CryptoJS.lib.WordArray.random(16);
20
25
  const ivHex = iv.toString(CryptoJS.enc.Hex);
21
26
  const ivWA = CryptoJS.enc.Hex.parse(ivHex);
@@ -71,6 +76,9 @@ var PataLinkClient = class {
71
76
  if (request.paymentMethod !== "PesaPal" && !request.phoneNumber) {
72
77
  throw new Error("phoneNumber is required for mobile money payments");
73
78
  }
79
+ if (request.paymentMethod === "PesaPal" && !request.callbackUrl) {
80
+ throw new Error("callbackUrl is required for PesaPal (card) payments");
81
+ }
74
82
  if (request.amount < 100) {
75
83
  throw new Error("Amount must be at least 100 RWF");
76
84
  }
@@ -85,6 +93,9 @@ var PataLinkClient = class {
85
93
  supporterName: request.customerName || "",
86
94
  supporterEmail: request.customerEmail || ""
87
95
  };
96
+ if (request.paymentMethod === "PesaPal" && request.callbackUrl) {
97
+ paymentData.callbackUrl = request.callbackUrl;
98
+ }
88
99
  const encryptedPayload = this.encrypt(paymentData);
89
100
  const response = await this.request("/api/pay", {
90
101
  method: "POST",
@@ -107,98 +118,77 @@ var PataLinkClient = class {
107
118
  /**
108
119
  * Poll for transaction completion
109
120
  */
110
- // async waitForCompletion(
111
- // transactionId: string,
112
- // options?: {
113
- // maxAttempts?: number;
114
- // interval?: number;
115
- // onProgress?: (attempt: number, status: string) => void;
116
- // }
117
- // ): Promise<TransactionStatus> {
118
- // const maxAttempts = options?.maxAttempts || 60;
119
- // const interval = options?.interval || 2000;
120
- // let attempts = 0;
121
- // while (attempts < maxAttempts) {
122
- // attempts++;
123
- // options?.onProgress?.(attempts, 'checking');
124
- // const status = await this.getTransactionStatus(transactionId);
125
- // if (status.status === 'success' || status.status === 'failed') {
126
- // return status;
127
- // }
128
- // // Wait before next attempt
129
- // await new Promise(resolve => setTimeout(resolve, interval));
130
- // }
131
- // throw new Error('Transaction timeout - still pending after maximum attempts');
132
- // }
121
+ async waitForCompletion(transactionId, options) {
122
+ const maxAttempts = options?.maxAttempts || 60;
123
+ const interval = options?.interval || 2e3;
124
+ let attempts = 0;
125
+ while (attempts < maxAttempts) {
126
+ attempts++;
127
+ options?.onProgress?.(attempts, "checking");
128
+ const status = await this.getTransactionStatus(transactionId);
129
+ if (status.status === "success" || status.status === "failed") {
130
+ return status;
131
+ }
132
+ await new Promise((resolve) => setTimeout(resolve, interval));
133
+ }
134
+ throw new Error("Transaction timeout - still pending after maximum attempts");
135
+ }
133
136
  /**
134
137
  * List all transactions for this account
135
138
  */
136
- // async listTransactions(options?: {
137
- // limit?: number;
138
- // offset?: number;
139
- // status?: 'pending' | 'success' | 'failed';
140
- // }): Promise<{ transactions: TransactionStatus[]; total: number }> {
141
- // const params = new URLSearchParams();
142
- // if (options?.limit) params.append('limit', options.limit.toString());
143
- // if (options?.offset) params.append('offset', options.offset.toString());
144
- // if (options?.status) params.append('status', options.status);
145
- // const query = params.toString();
146
- // const endpoint = `/v1/transactions${query ? `?${query}` : ''}`;
147
- // return this.request<{ transactions: TransactionStatus[]; total: number }>(endpoint, {
148
- // method: 'GET'
149
- // });
150
- // }
139
+ async listTransactions(options) {
140
+ const params = new URLSearchParams();
141
+ if (options?.limit) params.append("limit", options.limit.toString());
142
+ if (options?.offset) params.append("offset", options.offset.toString());
143
+ if (options?.status) params.append("status", options.status);
144
+ const query = params.toString();
145
+ const endpoint = `/api/transactions${query ? `?${query}` : ""}`;
146
+ return this.request(endpoint, {
147
+ method: "GET"
148
+ });
149
+ }
151
150
  /**
152
151
  * Rotate encryption key (generates new key, revokes old one after 24h)
153
152
  */
154
- // async rotateEncryptionKey(): Promise<{ newEncryptionKey: string; message: string }> {
155
- // return this.request<{ newEncryptionKey: string; message: string }>('/v1/keys/rotate', {
156
- // method: 'POST'
157
- // });
158
- // }
153
+ async rotateEncryptionKey() {
154
+ return this.request("/api/keys/rotate", {
155
+ method: "POST"
156
+ });
157
+ }
159
158
  /**
160
159
  * Get account information
161
160
  */
162
- // async getAccountInfo(): Promise<{
163
- // id: string;
164
- // email: string;
165
- // walletBalance: number;
166
- // pendingBalance: number;
167
- // availableBalance: number;
168
- // }> {
169
- // return this.request('/v1/account', { method: 'GET' });
170
- // }
161
+ async getAccountInfo() {
162
+ return this.request("/api/account", { method: "GET" });
163
+ }
171
164
  /**
172
165
  * Initiate withdrawal
173
166
  */
174
- // async createWithdrawal(request: {
175
- // amount: number;
176
- // phoneNumber: string;
177
- // network: 'MTN' | 'Airtel';
178
- // }): Promise<{ withdrawalId: string; status: string; message: string }> {
179
- // if (!request.amount) throw new Error('amount is required');
180
- // if (!request.phoneNumber) throw new Error('phoneNumber is required');
181
- // if (request.amount < 100) throw new Error('Minimum withdrawal is 100 RWF');
182
- // if (request.amount > 2000000) throw new Error('Maximum withdrawal is 2,000,000 RWF');
183
- // return this.request('/v1/withdrawals', {
184
- // method: 'POST',
185
- // body: JSON.stringify(request)
186
- // });
187
- // }
167
+ async createWithdrawal(request) {
168
+ if (!request.amount) throw new Error("amount is required");
169
+ if (!request.phoneNumber) throw new Error("phoneNumber is required");
170
+ if (request.amount < 100) throw new Error("Minimum withdrawal is 100 RWF");
171
+ if (request.amount > 2e6) throw new Error("Maximum withdrawal is 2,000,000 RWF");
172
+ const encryptedPayload = this.encrypt(request);
173
+ return this.request("/api/withdrawals", {
174
+ method: "POST",
175
+ headers: {
176
+ "x-encrypted": "true"
177
+ },
178
+ body: JSON.stringify(encryptedPayload)
179
+ });
180
+ }
188
181
  /**
189
182
  * List withdrawals
190
183
  */
191
- // async listWithdrawals(options?: {
192
- // limit?: number;
193
- // offset?: number;
194
- // }): Promise<{ withdrawals: any[]; total: number }> {
195
- // const params = new URLSearchParams();
196
- // if (options?.limit) params.append('limit', options.limit.toString());
197
- // if (options?.offset) params.append('offset', options.offset.toString());
198
- // const query = params.toString();
199
- // const endpoint = `/v1/withdrawals${query ? `?${query}` : ''}`;
200
- // return this.request(endpoint, { method: 'GET' });
201
- // }
184
+ async listWithdrawals(options) {
185
+ const params = new URLSearchParams();
186
+ if (options?.limit) params.append("limit", options.limit.toString());
187
+ if (options?.offset) params.append("offset", options.offset.toString());
188
+ const query = params.toString();
189
+ const endpoint = `/api/withdrawals${query ? `?${query}` : ""}`;
190
+ return this.request(endpoint, { method: "GET" });
191
+ }
202
192
  };
203
193
  function createClient(config) {
204
194
  return new PataLinkClient(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patalink/node-api-client",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Official PataLink API client - Simple, secure payment integration",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",