@abdssamie/adyen-payments 0.1.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.
Files changed (69) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +258 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/index.d.ts +206 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +566 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +36 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +215 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +121 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +78 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +3 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/private.d.ts +71 -0
  32. package/dist/component/private.d.ts.map +1 -0
  33. package/dist/component/private.js +250 -0
  34. package/dist/component/private.js.map +1 -0
  35. package/dist/component/public.d.ts +170 -0
  36. package/dist/component/public.d.ts.map +1 -0
  37. package/dist/component/public.js +210 -0
  38. package/dist/component/public.js.map +1 -0
  39. package/dist/component/schema.d.ts +101 -0
  40. package/dist/component/schema.d.ts.map +1 -0
  41. package/dist/component/schema.js +63 -0
  42. package/dist/component/schema.js.map +1 -0
  43. package/dist/react/hooks.d.ts +182 -0
  44. package/dist/react/hooks.d.ts.map +1 -0
  45. package/dist/react/hooks.js +215 -0
  46. package/dist/react/hooks.js.map +1 -0
  47. package/dist/react/index.d.ts +3 -0
  48. package/dist/react/index.d.ts.map +1 -0
  49. package/dist/react/index.js +3 -0
  50. package/dist/react/index.js.map +1 -0
  51. package/package.json +104 -0
  52. package/src/client/_generated/_ignore.ts +1 -0
  53. package/src/client/index.test.ts +196 -0
  54. package/src/client/index.ts +823 -0
  55. package/src/client/setup.test.ts +26 -0
  56. package/src/client/webhooks.test.ts +182 -0
  57. package/src/component/_generated/api.ts +52 -0
  58. package/src/component/_generated/component.ts +293 -0
  59. package/src/component/_generated/dataModel.ts +60 -0
  60. package/src/component/_generated/server.ts +156 -0
  61. package/src/component/convex.config.ts +3 -0
  62. package/src/component/private.ts +277 -0
  63. package/src/component/public.test.ts +92 -0
  64. package/src/component/public.ts +229 -0
  65. package/src/component/schema.ts +67 -0
  66. package/src/component/setup.test.ts +11 -0
  67. package/src/react/hooks.ts +488 -0
  68. package/src/react/index.ts +18 -0
  69. package/src/test.ts +18 -0
@@ -0,0 +1,488 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useAction, useQuery } from "convex/react";
5
+ import type {
6
+ FunctionReference,
7
+ } from "convex/server";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Shared Types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface StoredCard {
14
+ shopperReference: string;
15
+ recurringDetailReference: string;
16
+ variant: string;
17
+ cardLast4?: string;
18
+ cardExpiryMonth?: string;
19
+ cardExpiryYear?: string;
20
+ status: string;
21
+ metadata?: Record<string, unknown>;
22
+ }
23
+
24
+ export interface PaymentTransaction {
25
+ pspReference: string;
26
+ originalReference?: string;
27
+ shopperReference?: string;
28
+ merchantReference: string;
29
+ amount: number;
30
+ currency: string;
31
+ status: string;
32
+ paymentMethod?: string;
33
+ created: number;
34
+ metadata?: Record<string, unknown>;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Config shape required by all hooks
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * The Convex API references the hooks need.
43
+ * Pass in the `api` object generated for your example app.
44
+ *
45
+ * Example:
46
+ * ```ts
47
+ * import { api } from "../convex/_generated/api";
48
+ * const config: AdyenHooksConfig = { api };
49
+ * ```
50
+ */
51
+ export interface AdyenHooksConfig {
52
+ queries: {
53
+ getShopper: FunctionReference<"query">;
54
+ listPaymentMethods: FunctionReference<"query">;
55
+ listPayments: FunctionReference<"query">;
56
+ getPayment: FunctionReference<"query">;
57
+ };
58
+ actions: {
59
+ getOrCreateShopper: FunctionReference<"action">;
60
+ createCheckout: FunctionReference<"action">;
61
+ syncPaymentMethods: FunctionReference<"action">;
62
+ deletePaymentMethod: FunctionReference<"action">;
63
+ chargeCard: FunctionReference<"action">;
64
+ capture: FunctionReference<"action">;
65
+ refund: FunctionReference<"action">;
66
+ cancel: FunctionReference<"action">;
67
+ };
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // useAdyenShopper
72
+ // ---------------------------------------------------------------------------
73
+
74
+ interface UseAdyenShopperOptions {
75
+ shopperReference: string;
76
+ config: AdyenHooksConfig;
77
+ }
78
+
79
+ interface UseAdyenShopperResult {
80
+ shopper: Record<string, unknown> | null | undefined;
81
+ isLoading: boolean;
82
+ error: string | null;
83
+ register: (args: {
84
+ userId: string;
85
+ email?: string;
86
+ name?: string;
87
+ }) => Promise<void>;
88
+ }
89
+
90
+ /**
91
+ * Manages a shopper identity: reads their record from Convex and provides
92
+ * a `register` function to upsert them via the Adyen component.
93
+ */
94
+ export function useAdyenShopper({
95
+ shopperReference,
96
+ config,
97
+ }: UseAdyenShopperOptions): UseAdyenShopperResult {
98
+ const [isLoading, setIsLoading] = useState(false);
99
+ const [error, setError] = useState<string | null>(null);
100
+
101
+ const shopper = useQuery(config.queries.getShopper, { shopperReference }) as
102
+ | Record<string, unknown>
103
+ | null
104
+ | undefined;
105
+
106
+ const getOrCreateShopperAction = useAction(config.actions.getOrCreateShopper);
107
+
108
+ const register = useCallback(
109
+ async (args: { userId: string; email?: string; name?: string }) => {
110
+ setIsLoading(true);
111
+ setError(null);
112
+ try {
113
+ await getOrCreateShopperAction(args);
114
+ } catch (err: unknown) {
115
+ const message = err instanceof Error ? err.message : "Failed to register shopper";
116
+ setError(message);
117
+ throw err;
118
+ } finally {
119
+ setIsLoading(false);
120
+ }
121
+ },
122
+ [getOrCreateShopperAction]
123
+ );
124
+
125
+ return { shopper, isLoading, error, register };
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // useStoredPaymentMethods
130
+ // ---------------------------------------------------------------------------
131
+
132
+ interface UseStoredPaymentMethodsOptions {
133
+ shopperReference: string;
134
+ config: AdyenHooksConfig;
135
+ }
136
+
137
+ interface UseStoredPaymentMethodsResult {
138
+ paymentMethods: StoredCard[];
139
+ isLoading: boolean;
140
+ isSyncing: boolean;
141
+ error: string | null;
142
+ sync: () => Promise<void>;
143
+ remove: (recurringDetailReference: string) => Promise<void>;
144
+ }
145
+
146
+ /**
147
+ * Reads stored (tokenised) payment methods for a shopper from Convex,
148
+ * and provides helpers to sync from Adyen or delete a token.
149
+ */
150
+ export function useStoredPaymentMethods({
151
+ shopperReference,
152
+ config,
153
+ }: UseStoredPaymentMethodsOptions): UseStoredPaymentMethodsResult {
154
+ const [isSyncing, setIsSyncing] = useState(false);
155
+ const [isLoading, setIsLoading] = useState(false);
156
+ const [error, setError] = useState<string | null>(null);
157
+
158
+ const paymentMethods = (useQuery(config.queries.listPaymentMethods, {
159
+ shopperReference,
160
+ }) ?? []) as StoredCard[];
161
+
162
+ const syncAction = useAction(config.actions.syncPaymentMethods);
163
+ const deleteAction = useAction(config.actions.deletePaymentMethod);
164
+
165
+ const sync = useCallback(async () => {
166
+ setIsSyncing(true);
167
+ setError(null);
168
+ try {
169
+ await syncAction({ shopperReference });
170
+ } catch (err: unknown) {
171
+ const message = err instanceof Error ? err.message : "Failed to sync payment methods";
172
+ setError(message);
173
+ throw err;
174
+ } finally {
175
+ setIsSyncing(false);
176
+ }
177
+ }, [syncAction, shopperReference]);
178
+
179
+ const remove = useCallback(
180
+ async (recurringDetailReference: string) => {
181
+ setIsLoading(true);
182
+ setError(null);
183
+ try {
184
+ await deleteAction({ shopperReference, recurringDetailReference });
185
+ } catch (err: unknown) {
186
+ const message = err instanceof Error ? err.message : "Failed to remove payment method";
187
+ setError(message);
188
+ throw err;
189
+ } finally {
190
+ setIsLoading(false);
191
+ }
192
+ },
193
+ [deleteAction, shopperReference]
194
+ );
195
+
196
+ return { paymentMethods, isLoading, isSyncing, error, sync, remove };
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // usePayments
201
+ // ---------------------------------------------------------------------------
202
+
203
+ interface UsePaymentsOptions {
204
+ shopperReference: string;
205
+ config: AdyenHooksConfig;
206
+ }
207
+
208
+ interface UsePaymentsResult {
209
+ payments: PaymentTransaction[];
210
+ }
211
+
212
+ /**
213
+ * Reactive list of payment transactions for a shopper, automatically kept
214
+ * up to date by Convex's live query subscription.
215
+ */
216
+ export function usePayments({
217
+ shopperReference,
218
+ config,
219
+ }: UsePaymentsOptions): UsePaymentsResult {
220
+ const payments = (useQuery(config.queries.listPayments, {
221
+ shopperReference,
222
+ }) ?? []) as PaymentTransaction[];
223
+
224
+ return { payments };
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // usePaymentOperations
229
+ // ---------------------------------------------------------------------------
230
+
231
+ export interface PaymentOperations {
232
+ chargeCard: (args: {
233
+ shopperReference: string;
234
+ recurringDetailReference: string;
235
+ amount: number;
236
+ currency: string;
237
+ }) => Promise<{ pspReference: string | null; status: string; resultCode: string }>;
238
+ capture: (args: {
239
+ pspReference: string;
240
+ amount: number;
241
+ currency: string;
242
+ }) => Promise<void>;
243
+ refund: (args: {
244
+ pspReference: string;
245
+ amount: number;
246
+ currency: string;
247
+ }) => Promise<void>;
248
+ cancel: (args: { pspReference: string }) => Promise<void>;
249
+ createCheckoutSession: (args: {
250
+ amount: number;
251
+ currency: string;
252
+ shopperReference?: string;
253
+ }) => Promise<{ sessionId: string; sessionData: string; url: string | null }>;
254
+ isLoading: (key: string) => boolean;
255
+ error: string | null;
256
+ clearError: () => void;
257
+ }
258
+
259
+ /**
260
+ * Provides callable async helpers for all payment lifecycle operations —
261
+ * checkout session creation, MIT charges, captures, refunds, and cancellations.
262
+ * Tracks per-operation loading state via a string key.
263
+ */
264
+ export function usePaymentOperations(config: AdyenHooksConfig): PaymentOperations {
265
+ const [loadingKeys, setLoadingKeys] = useState<Set<string>>(new Set());
266
+ const [error, setError] = useState<string | null>(null);
267
+
268
+ const chargeCardAction = useAction(config.actions.chargeCard);
269
+ const captureAction = useAction(config.actions.capture);
270
+ const refundAction = useAction(config.actions.refund);
271
+ const cancelAction = useAction(config.actions.cancel);
272
+ const createCheckoutAction = useAction(config.actions.createCheckout);
273
+
274
+ const withLoading = useCallback(
275
+ async <T>(key: string, fn: () => Promise<T>): Promise<T> => {
276
+ setLoadingKeys((prev) => new Set(prev).add(key));
277
+ setError(null);
278
+ try {
279
+ return await fn();
280
+ } catch (err: unknown) {
281
+ const message = err instanceof Error ? err.message : "An error occurred";
282
+ setError(message);
283
+ throw err;
284
+ } finally {
285
+ setLoadingKeys((prev) => {
286
+ const next = new Set(prev);
287
+ next.delete(key);
288
+ return next;
289
+ });
290
+ }
291
+ },
292
+ []
293
+ );
294
+
295
+ const chargeCard = useCallback(
296
+ (args: {
297
+ shopperReference: string;
298
+ recurringDetailReference: string;
299
+ amount: number;
300
+ currency: string;
301
+ }) =>
302
+ withLoading(`charge:${args.recurringDetailReference}`, () =>
303
+ chargeCardAction(args) as Promise<{
304
+ pspReference: string | null;
305
+ status: string;
306
+ resultCode: string;
307
+ }>
308
+ ),
309
+ [chargeCardAction, withLoading]
310
+ );
311
+
312
+ const capture = useCallback(
313
+ (args: { pspReference: string; amount: number; currency: string }) =>
314
+ withLoading(`capture:${args.pspReference}`, () =>
315
+ captureAction(args).then(() => undefined)
316
+ ),
317
+ [captureAction, withLoading]
318
+ );
319
+
320
+ const refund = useCallback(
321
+ (args: { pspReference: string; amount: number; currency: string }) =>
322
+ withLoading(`refund:${args.pspReference}`, () =>
323
+ refundAction(args).then(() => undefined)
324
+ ),
325
+ [refundAction, withLoading]
326
+ );
327
+
328
+ const cancel = useCallback(
329
+ (args: { pspReference: string }) =>
330
+ withLoading(`cancel:${args.pspReference}`, () =>
331
+ cancelAction(args).then(() => undefined)
332
+ ),
333
+ [cancelAction, withLoading]
334
+ );
335
+
336
+ const createCheckoutSession = useCallback(
337
+ (args: { amount: number; currency: string; shopperReference?: string }) =>
338
+ withLoading("checkout", () =>
339
+ createCheckoutAction(args) as Promise<{
340
+ sessionId: string;
341
+ sessionData: string;
342
+ url: string | null;
343
+ }>
344
+ ),
345
+ [createCheckoutAction, withLoading]
346
+ );
347
+
348
+ const isLoading = useCallback(
349
+ (key: string) => loadingKeys.has(key),
350
+ [loadingKeys]
351
+ );
352
+
353
+ const clearError = useCallback(() => setError(null), []);
354
+
355
+ return {
356
+ chargeCard,
357
+ capture,
358
+ refund,
359
+ cancel,
360
+ createCheckoutSession,
361
+ isLoading,
362
+ error,
363
+ clearError,
364
+ };
365
+ }
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // useAdyenDropin
369
+ // ---------------------------------------------------------------------------
370
+
371
+ export interface UseAdyenDropinOptions {
372
+ /** Adyen client key (ADYEN_CLIENT_KEY) — safe to expose on the frontend */
373
+ clientKey: string;
374
+ /** Session ID returned by createCheckoutSession */
375
+ sessionId: string | null;
376
+ /** Session data blob returned by createCheckoutSession */
377
+ sessionData: string | null;
378
+ /** Environment: "TEST" or "LIVE" */
379
+ environment?: "test" | "live";
380
+ /** Shopper country code (e.g. "US", "NL") — required by some payment methods */
381
+ countryCode?: string;
382
+ /** Callback when Adyen reports payment completion */
383
+ onPaymentCompleted?: (result: { resultCode: string }) => void;
384
+ /** Callback when Adyen reports an error */
385
+ onError?: (error: { name: string; message: string }) => void;
386
+ }
387
+
388
+ export interface UseAdyenDropinResult {
389
+ /** Ref to attach to a container div — Adyen mounts the Drop-in here */
390
+ containerRef: React.RefObject<HTMLDivElement | null>;
391
+ isReady: boolean;
392
+ mountError: string | null;
393
+ }
394
+
395
+ /**
396
+ * Mounts the Adyen Drop-in UI component into a provided container div.
397
+ * Automatically initialises or re-initialises when sessionId/sessionData changes.
398
+ *
399
+ * Usage:
400
+ * ```tsx
401
+ * const { containerRef } = useAdyenDropin({ clientKey, sessionId, sessionData });
402
+ * return <div ref={containerRef} />;
403
+ * ```
404
+ */
405
+ export function useAdyenDropin({
406
+ clientKey,
407
+ sessionId,
408
+ sessionData,
409
+ environment = "test",
410
+ countryCode,
411
+ onPaymentCompleted,
412
+ onError,
413
+ }: UseAdyenDropinOptions): UseAdyenDropinResult {
414
+ const containerRef = useRef<HTMLDivElement | null>(null);
415
+ const dropinRef = useRef<{ unmount?: () => void } | null>(null);
416
+ const [isReady, setIsReady] = useState(false);
417
+ const [mountError, setMountError] = useState<string | null>(null);
418
+ const onPaymentCompletedRef = useRef(onPaymentCompleted);
419
+ const onErrorRef = useRef(onError);
420
+
421
+ useEffect(() => {
422
+ onPaymentCompletedRef.current = onPaymentCompleted;
423
+ onErrorRef.current = onError;
424
+ });
425
+
426
+ useEffect(() => {
427
+ if (!sessionId || !sessionData || !containerRef.current || !clientKey) {
428
+ return;
429
+ }
430
+
431
+ let cancelled = false;
432
+
433
+ const mountDropin = async () => {
434
+ try {
435
+ // Dynamically import to avoid SSR issues and keep bundle lean
436
+ const { AdyenCheckout, Dropin, Card } = await import("@adyen/adyen-web");
437
+
438
+ if (cancelled || !containerRef.current) return;
439
+
440
+ // Unmount previous instance if any
441
+ if (dropinRef.current?.unmount) {
442
+ dropinRef.current.unmount();
443
+ }
444
+
445
+ const checkout = await AdyenCheckout({
446
+ environment,
447
+ clientKey,
448
+ countryCode,
449
+ session: { id: sessionId, sessionData },
450
+ onPaymentCompleted: (result: { resultCode: string }) => {
451
+ if (!cancelled) onPaymentCompletedRef.current?.(result);
452
+ },
453
+ onError: (err: { name: string; message: string }) => {
454
+ if (!cancelled) onErrorRef.current?.(err);
455
+ },
456
+ });
457
+
458
+ if (cancelled || !containerRef.current) return;
459
+
460
+ // v6 API: instantiate Dropin with the Core instance, then mount
461
+ const dropin = new Dropin(checkout, {
462
+ paymentMethodComponents: [Card],
463
+ }).mount(containerRef.current);
464
+ dropinRef.current = dropin as { unmount?: () => void };
465
+ if (!cancelled) setIsReady(true);
466
+ } catch (err: unknown) {
467
+ if (!cancelled) {
468
+ const message = err instanceof Error ? err.message : "Failed to mount Adyen Drop-in";
469
+ setMountError(message);
470
+ }
471
+ }
472
+ };
473
+
474
+ setIsReady(false);
475
+ setMountError(null);
476
+ void mountDropin();
477
+
478
+ return () => {
479
+ cancelled = true;
480
+ if (dropinRef.current?.unmount) {
481
+ dropinRef.current.unmount();
482
+ dropinRef.current = null;
483
+ }
484
+ };
485
+ }, [clientKey, sessionId, sessionData, environment, countryCode]);
486
+
487
+ return { containerRef, isReady, mountError };
488
+ }
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ export {
4
+ useAdyenShopper,
5
+ useStoredPaymentMethods,
6
+ usePayments,
7
+ usePaymentOperations,
8
+ useAdyenDropin,
9
+ } from "./hooks.js";
10
+
11
+ export type {
12
+ AdyenHooksConfig,
13
+ StoredCard,
14
+ PaymentTransaction,
15
+ PaymentOperations,
16
+ UseAdyenDropinOptions,
17
+ UseAdyenDropinResult,
18
+ } from "./hooks.js";
package/src/test.ts ADDED
@@ -0,0 +1,18 @@
1
+ /// <reference types="vite/client" />
2
+ import type { TestConvex } from "convex-test";
3
+ import type { GenericSchema, SchemaDefinition } from "convex/server";
4
+ import schema from "./component/schema.js";
5
+ const modules = import.meta.glob("./component/**/*.ts");
6
+
7
+ /**
8
+ * Register the component with the test convex instance.
9
+ * @param t - The test convex instance, e.g. from calling `convexTest`.
10
+ * @param name - The name of the component, as registered in convex.config.ts.
11
+ */
12
+ export function register(
13
+ t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
14
+ name: string = "adyenPayments",
15
+ ) {
16
+ t.registerComponent(name, schema, modules);
17
+ }
18
+ export default { register, schema, modules };