@henrylabs-interview/payment-processor 0.1.8 → 0.1.10
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/dist/index.d.ts +14 -1
- package/dist/index.js +20 -1
- package/dist/resources/checkout.d.ts +9 -0
- package/dist/resources/checkout.js +30 -8
- package/dist/resources/embedded.d.ts +1 -0
- package/dist/resources/embedded.js +165 -0
- package/dist/utils/store.d.ts +1 -2
- package/dist/utils/store.js +23 -6
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Checkout } from './resources/checkout';
|
|
2
2
|
import { Webhooks } from './resources/webhooks';
|
|
3
|
-
export
|
|
3
|
+
export declare class PaymentProcessorSDK {
|
|
4
4
|
checkout: Checkout;
|
|
5
5
|
webhooks: Webhooks;
|
|
6
6
|
private VALID_API_KEYS;
|
|
@@ -8,3 +8,16 @@ export default class HenryPaymentProcessorSDK {
|
|
|
8
8
|
apiKey: string;
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
|
+
export declare class EmbeddedCheckoutUI {
|
|
12
|
+
private checkoutId;
|
|
13
|
+
constructor(config: {
|
|
14
|
+
checkoutId: string;
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Renders the embedded checkout UI
|
|
18
|
+
* @param containerElementId - The ID of the container element where the checkout UI will be rendered
|
|
19
|
+
* @param callbackFn - Callback function to handle the payment token
|
|
20
|
+
* @returns - Whether the UI rendered successfully
|
|
21
|
+
*/
|
|
22
|
+
render(containerElementId: string, callbackFn: (paymentToken: string) => void): boolean;
|
|
23
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Checkout } from './resources/checkout';
|
|
2
|
+
import { renderEmbeddedCheckout } from './resources/embedded';
|
|
2
3
|
import { Webhooks } from './resources/webhooks';
|
|
3
|
-
export
|
|
4
|
+
export class PaymentProcessorSDK {
|
|
4
5
|
checkout;
|
|
5
6
|
webhooks;
|
|
6
7
|
VALID_API_KEYS = ['824c951e-dfac-4342-8e03', '3f67e17a-880a-463b-a667', '78254d40-623a-48c5-b83f'];
|
|
@@ -15,3 +16,21 @@ export default class HenryPaymentProcessorSDK {
|
|
|
15
16
|
this.webhooks = new Webhooks();
|
|
16
17
|
}
|
|
17
18
|
}
|
|
19
|
+
export class EmbeddedCheckoutUI {
|
|
20
|
+
checkoutId;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
if (!config.checkoutId) {
|
|
23
|
+
throw new Error('Henry: checkoutId is required');
|
|
24
|
+
}
|
|
25
|
+
this.checkoutId = config.checkoutId;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Renders the embedded checkout UI
|
|
29
|
+
* @param containerElementId - The ID of the container element where the checkout UI will be rendered
|
|
30
|
+
* @param callbackFn - Callback function to handle the payment token
|
|
31
|
+
* @returns - Whether the UI rendered successfully
|
|
32
|
+
*/
|
|
33
|
+
render(containerElementId, callbackFn) {
|
|
34
|
+
return renderEmbeddedCheckout(containerElementId, this.checkoutId, callbackFn);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -64,6 +64,15 @@ interface CheckoutConfirmFailure extends CheckoutConfirmGeneric {
|
|
|
64
64
|
status: 'failure';
|
|
65
65
|
substatus: '500-error' | '502-fraud' | '503-retry';
|
|
66
66
|
}
|
|
67
|
+
export declare const INTERNAL_CHECKOUTS: Record<string, {
|
|
68
|
+
historyRecordId: number;
|
|
69
|
+
}>;
|
|
70
|
+
export declare const INTERNAL_CARDS: Record<string, {
|
|
71
|
+
number: string;
|
|
72
|
+
expMonth: number;
|
|
73
|
+
expYear: number;
|
|
74
|
+
cvc: string;
|
|
75
|
+
}>;
|
|
67
76
|
export declare class Checkout {
|
|
68
77
|
/**
|
|
69
78
|
* Create a new checkout session
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { readHistory } from '../utils/store';
|
|
1
|
+
import { readHistory, writeHistory } from '../utils/store';
|
|
2
2
|
import { generateID, hashToNumber, signPayload } from '../utils/crypto';
|
|
3
3
|
import { INTERNAL_WEBHOOKS } from './webhooks';
|
|
4
4
|
import { sleep } from '../utils/async';
|
|
5
5
|
// checkoutId -> { historyRecordId }
|
|
6
|
-
const INTERNAL_CHECKOUTS = {};
|
|
6
|
+
export const INTERNAL_CHECKOUTS = {};
|
|
7
7
|
// paymentToken -> card details
|
|
8
|
-
const INTERNAL_CARDS = {};
|
|
8
|
+
export const INTERNAL_CARDS = {};
|
|
9
9
|
export class Checkout {
|
|
10
10
|
/**
|
|
11
11
|
* Create a new checkout session
|
|
@@ -31,6 +31,13 @@ export class Checkout {
|
|
|
31
31
|
// Phase 3: Webhook Scheduling
|
|
32
32
|
// ---------------------------------------
|
|
33
33
|
this.scheduleCreateWebhook(hashId, response);
|
|
34
|
+
// Add to history records
|
|
35
|
+
await writeHistory({
|
|
36
|
+
id: hashId,
|
|
37
|
+
amount: params.amount,
|
|
38
|
+
currency: params.currency,
|
|
39
|
+
customerId: params.customerId ?? null,
|
|
40
|
+
});
|
|
34
41
|
// Simulate API latency
|
|
35
42
|
await sleep(Math.random() * 2000);
|
|
36
43
|
return response;
|
|
@@ -126,7 +133,7 @@ export class Checkout {
|
|
|
126
133
|
}
|
|
127
134
|
createCheckoutRecord(hashId) {
|
|
128
135
|
const checkoutId = generateID();
|
|
129
|
-
INTERNAL_CHECKOUTS[checkoutId] = {
|
|
136
|
+
INTERNAL_CHECKOUTS[`${checkoutId}`] = {
|
|
130
137
|
historyRecordId: hashId,
|
|
131
138
|
};
|
|
132
139
|
return checkoutId;
|
|
@@ -174,7 +181,7 @@ export class Checkout {
|
|
|
174
181
|
}
|
|
175
182
|
///
|
|
176
183
|
async validateConfirm(params) {
|
|
177
|
-
if (!INTERNAL_CHECKOUTS[params.checkoutId]) {
|
|
184
|
+
if (!INTERNAL_CHECKOUTS[`${params.checkoutId}`]) {
|
|
178
185
|
return {
|
|
179
186
|
status: 'failure',
|
|
180
187
|
substatus: '500-error',
|
|
@@ -183,7 +190,7 @@ export class Checkout {
|
|
|
183
190
|
};
|
|
184
191
|
}
|
|
185
192
|
if (params.type === 'embedded') {
|
|
186
|
-
const stored = INTERNAL_CARDS[params.data.paymentToken];
|
|
193
|
+
const stored = INTERNAL_CARDS[`${params.data.paymentToken}`];
|
|
187
194
|
if (!stored) {
|
|
188
195
|
return {
|
|
189
196
|
status: 'failure',
|
|
@@ -223,6 +230,21 @@ export class Checkout {
|
|
|
223
230
|
return null;
|
|
224
231
|
}
|
|
225
232
|
async processConfirmDecision(params) {
|
|
233
|
+
if (params.type === 'raw-card') {
|
|
234
|
+
// Add new card to internal storage
|
|
235
|
+
const cardDetails = {
|
|
236
|
+
number: params.data.number,
|
|
237
|
+
expMonth: params.data.expMonth,
|
|
238
|
+
expYear: params.data.expYear > 2000 ? params.data.expYear : params.data.expYear + 2000,
|
|
239
|
+
cvc: params.data.cvc,
|
|
240
|
+
};
|
|
241
|
+
const paymentToken = 'pmt_' +
|
|
242
|
+
hashToNumber(JSON.stringify({
|
|
243
|
+
type: 'PAYMENT_TOKEN',
|
|
244
|
+
...cardDetails,
|
|
245
|
+
}));
|
|
246
|
+
INTERNAL_CARDS[`${paymentToken}`] = cardDetails;
|
|
247
|
+
}
|
|
226
248
|
const decision = Math.random();
|
|
227
249
|
if (decision > 0.85) {
|
|
228
250
|
return {
|
|
@@ -251,7 +273,7 @@ export class Checkout {
|
|
|
251
273
|
return this.buildInstantConfirmSuccess(params.checkoutId);
|
|
252
274
|
}
|
|
253
275
|
async buildInstantConfirmSuccess(checkoutId) {
|
|
254
|
-
const { historyRecordId } = INTERNAL_CHECKOUTS[checkoutId] ?? {};
|
|
276
|
+
const { historyRecordId } = INTERNAL_CHECKOUTS[`${checkoutId}`] ?? {};
|
|
255
277
|
if (!historyRecordId) {
|
|
256
278
|
return {
|
|
257
279
|
status: 'failure',
|
|
@@ -403,7 +425,7 @@ export class Checkout {
|
|
|
403
425
|
'Content-Type': 'application/json',
|
|
404
426
|
};
|
|
405
427
|
if (hook.secret) {
|
|
406
|
-
headers['
|
|
428
|
+
headers['x-henry-signature'] = signPayload(payload, hook.secret);
|
|
407
429
|
}
|
|
408
430
|
await fetch(hook.url, {
|
|
409
431
|
method: 'POST',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderEmbeddedCheckout(containerElementId: string, checkoutId: string, callbackFn: (paymentToken: string) => void): boolean;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { hashToNumber } from '../utils/crypto';
|
|
2
|
+
import { INTERNAL_CARDS, INTERNAL_CHECKOUTS } from './checkout';
|
|
3
|
+
export function renderEmbeddedCheckout(containerElementId, checkoutId, callbackFn) {
|
|
4
|
+
// Ensure browser environment
|
|
5
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
6
|
+
console.warn('EmbeddedCheckoutUI can only be used in a browser environment.');
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const { historyRecordId } = INTERNAL_CHECKOUTS[`${checkoutId}`] ?? {};
|
|
10
|
+
if (!historyRecordId) {
|
|
11
|
+
console.warn(`Invalid checkout ID: "${checkoutId}".`);
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const container = document.getElementById(containerElementId);
|
|
15
|
+
if (!container) {
|
|
16
|
+
console.warn(`Container element "${containerElementId}" not found.`);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
container.innerHTML = `
|
|
20
|
+
<div style="
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
22
|
+
background: #ffffff;
|
|
23
|
+
border-radius: 16px;
|
|
24
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
|
|
25
|
+
padding: 24px;
|
|
26
|
+
max-width: 360px;
|
|
27
|
+
width: 100%;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
">
|
|
30
|
+
<h3 style="
|
|
31
|
+
margin: 0 0 20px 0;
|
|
32
|
+
font-size: 18px;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
color: #111827;
|
|
35
|
+
">
|
|
36
|
+
Secure Checkout
|
|
37
|
+
</h3>
|
|
38
|
+
|
|
39
|
+
<div style="display:flex; flex-direction:column; gap:14px;">
|
|
40
|
+
|
|
41
|
+
<div style="display:flex; flex-direction:column;">
|
|
42
|
+
<label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
|
|
43
|
+
Card Number
|
|
44
|
+
</label>
|
|
45
|
+
<input id="card-number" type="text" placeholder="4242 4242 4242 4242"
|
|
46
|
+
style="
|
|
47
|
+
padding: 10px 12px;
|
|
48
|
+
border-radius: 10px;
|
|
49
|
+
border: 1px solid #e5e7eb;
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
outline: none;
|
|
52
|
+
transition: border 0.2s, box-shadow 0.2s;
|
|
53
|
+
"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div style="display:flex; gap:10px;">
|
|
58
|
+
|
|
59
|
+
<div style="flex:1; display:flex; flex-direction:column;">
|
|
60
|
+
<label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
|
|
61
|
+
Exp. Month
|
|
62
|
+
</label>
|
|
63
|
+
<input id="exp-month" type="number" placeholder="12" min="1" max="12"
|
|
64
|
+
style="
|
|
65
|
+
padding: 10px 12px;
|
|
66
|
+
border-radius: 10px;
|
|
67
|
+
border: 1px solid #e5e7eb;
|
|
68
|
+
font-size: 14px;
|
|
69
|
+
outline: none;
|
|
70
|
+
transition: border 0.2s, box-shadow 0.2s;
|
|
71
|
+
"
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div style="flex:1; display:flex; flex-direction:column;">
|
|
76
|
+
<label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
|
|
77
|
+
Exp. Year
|
|
78
|
+
</label>
|
|
79
|
+
<input id="exp-year" type="number" placeholder="2030"
|
|
80
|
+
style="
|
|
81
|
+
padding: 10px 12px;
|
|
82
|
+
border-radius: 10px;
|
|
83
|
+
border: 1px solid #e5e7eb;
|
|
84
|
+
font-size: 14px;
|
|
85
|
+
outline: none;
|
|
86
|
+
transition: border 0.2s, box-shadow 0.2s;
|
|
87
|
+
"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div style="display:flex; flex-direction:column;">
|
|
94
|
+
<label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
|
|
95
|
+
CVC
|
|
96
|
+
</label>
|
|
97
|
+
<input id="cvc" type="text" placeholder="123"
|
|
98
|
+
style="
|
|
99
|
+
padding: 10px 12px;
|
|
100
|
+
border-radius: 10px;
|
|
101
|
+
border: 1px solid #e5e7eb;
|
|
102
|
+
font-size: 14px;
|
|
103
|
+
outline: none;
|
|
104
|
+
transition: border 0.2s, box-shadow 0.2s;
|
|
105
|
+
"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<button id="confirm-btn"
|
|
110
|
+
style="
|
|
111
|
+
margin-top: 8px;
|
|
112
|
+
padding: 12px;
|
|
113
|
+
border-radius: 12px;
|
|
114
|
+
border: none;
|
|
115
|
+
font-size: 14px;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
|
118
|
+
color: white;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
transition: transform 0.05s ease, box-shadow 0.2s ease;
|
|
121
|
+
box-shadow: 0 4px 14px rgba(79,70,229,0.3);
|
|
122
|
+
"
|
|
123
|
+
onmouseover="this.style.boxShadow='0 6px 18px rgba(79,70,229,0.4)'"
|
|
124
|
+
onmouseout="this.style.boxShadow='0 4px 14px rgba(79,70,229,0.3)'"
|
|
125
|
+
onmousedown="this.style.transform='scale(0.98)'"
|
|
126
|
+
onmouseup="this.style.transform='scale(1)'"
|
|
127
|
+
>
|
|
128
|
+
Confirm Payment
|
|
129
|
+
</button>
|
|
130
|
+
|
|
131
|
+
<p style="
|
|
132
|
+
font-size: 11px;
|
|
133
|
+
color: #9ca3af;
|
|
134
|
+
text-align: center;
|
|
135
|
+
margin-top: 8px;
|
|
136
|
+
">
|
|
137
|
+
🔒 Payments are securely processed
|
|
138
|
+
</p>
|
|
139
|
+
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
`;
|
|
143
|
+
const button = container.querySelector('#confirm-btn');
|
|
144
|
+
button?.addEventListener('click', () => {
|
|
145
|
+
const number = (container.querySelector('#card-number')?.value ?? '').replace(/\s+/g, '');
|
|
146
|
+
const expMonth = Number(container.querySelector('#exp-month')?.value);
|
|
147
|
+
const expYear = Number(container.querySelector('#exp-year')?.value);
|
|
148
|
+
const cvc = container.querySelector('#cvc')?.value ?? '';
|
|
149
|
+
// Add new card to internal storage
|
|
150
|
+
const cardDetails = {
|
|
151
|
+
number: number,
|
|
152
|
+
expMonth: expMonth,
|
|
153
|
+
expYear: expYear > 2000 ? expYear : expYear + 2000,
|
|
154
|
+
cvc: cvc,
|
|
155
|
+
};
|
|
156
|
+
const paymentToken = 'pmt_' +
|
|
157
|
+
hashToNumber(JSON.stringify({
|
|
158
|
+
type: 'PAYMENT_TOKEN',
|
|
159
|
+
...cardDetails,
|
|
160
|
+
}));
|
|
161
|
+
INTERNAL_CARDS[`${paymentToken}`] = cardDetails;
|
|
162
|
+
callbackFn(paymentToken);
|
|
163
|
+
});
|
|
164
|
+
return true;
|
|
165
|
+
}
|
package/dist/utils/store.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export type HistoryRecord = {
|
|
|
5
5
|
customerId: string | null;
|
|
6
6
|
updatedAt: number;
|
|
7
7
|
createdAt: number;
|
|
8
|
-
confirmed: boolean;
|
|
9
8
|
};
|
|
10
9
|
/**
|
|
11
10
|
* Reads all history records from the JSON store.
|
|
@@ -14,4 +13,4 @@ export declare function readHistory(): Promise<HistoryRecord[]>;
|
|
|
14
13
|
/**
|
|
15
14
|
* Writes a new record to the JSON store.
|
|
16
15
|
*/
|
|
17
|
-
export declare function writeHistory(params: Omit<HistoryRecord, 'updatedAt'>): Promise<HistoryRecord>;
|
|
16
|
+
export declare function writeHistory(params: Omit<HistoryRecord, 'createdAt' | 'updatedAt'>): Promise<HistoryRecord>;
|
package/dist/utils/store.js
CHANGED
|
@@ -22,11 +22,28 @@ export async function readHistory() {
|
|
|
22
22
|
*/
|
|
23
23
|
export async function writeHistory(params) {
|
|
24
24
|
const history = await readHistory();
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const existingIndex = history.findIndex((v) => v.id === params.id);
|
|
27
|
+
let record;
|
|
28
|
+
if (existingIndex !== -1) {
|
|
29
|
+
// Replace existing record
|
|
30
|
+
record = {
|
|
31
|
+
...history[existingIndex],
|
|
32
|
+
...params,
|
|
33
|
+
createdAt: history[existingIndex]?.createdAt ?? now,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
};
|
|
36
|
+
history[existingIndex] = record;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Add new record
|
|
40
|
+
record = {
|
|
41
|
+
...params,
|
|
42
|
+
createdAt: now,
|
|
43
|
+
updatedAt: now,
|
|
44
|
+
};
|
|
45
|
+
history.push(record);
|
|
46
|
+
}
|
|
30
47
|
await Bun.write(path, JSON.stringify(history, null, 2));
|
|
31
|
-
return
|
|
48
|
+
return record;
|
|
32
49
|
}
|