@rovela-ai/sdk 0.24.1 → 0.25.1

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 (128) hide show
  1. package/dist/admin/api/index.d.ts +1 -0
  2. package/dist/admin/api/index.d.ts.map +1 -1
  3. package/dist/admin/api/index.js +2 -0
  4. package/dist/admin/api/index.js.map +1 -1
  5. package/dist/admin/api/payment-providers.d.ts +69 -0
  6. package/dist/admin/api/payment-providers.d.ts.map +1 -0
  7. package/dist/admin/api/payment-providers.js +206 -0
  8. package/dist/admin/api/payment-providers.js.map +1 -0
  9. package/dist/admin/api/settings.d.ts.map +1 -1
  10. package/dist/admin/api/settings.js +4 -1
  11. package/dist/admin/api/settings.js.map +1 -1
  12. package/dist/admin/components/OrderDetails.d.ts.map +1 -1
  13. package/dist/admin/components/OrderDetails.js +11 -2
  14. package/dist/admin/components/OrderDetails.js.map +1 -1
  15. package/dist/admin/components/PaymentProviderPanels.d.ts +2 -0
  16. package/dist/admin/components/PaymentProviderPanels.d.ts.map +1 -0
  17. package/dist/admin/components/PaymentProviderPanels.js +265 -0
  18. package/dist/admin/components/PaymentProviderPanels.js.map +1 -0
  19. package/dist/admin/components/PaymentSettings.d.ts.map +1 -1
  20. package/dist/admin/components/PaymentSettings.js +7 -20
  21. package/dist/admin/components/PaymentSettings.js.map +1 -1
  22. package/dist/admin/components/emails/EmailSendingPanel.d.ts.map +1 -1
  23. package/dist/admin/components/emails/EmailSendingPanel.js +2 -2
  24. package/dist/admin/components/emails/EmailSendingPanel.js.map +1 -1
  25. package/dist/admin/components/index.d.ts +1 -0
  26. package/dist/admin/components/index.d.ts.map +1 -1
  27. package/dist/admin/components/index.js +1 -0
  28. package/dist/admin/components/index.js.map +1 -1
  29. package/dist/admin/integrations/registry.d.ts.map +1 -1
  30. package/dist/admin/integrations/registry.js +5 -73
  31. package/dist/admin/integrations/registry.js.map +1 -1
  32. package/dist/cart/store.d.ts +4 -1
  33. package/dist/cart/store.d.ts.map +1 -1
  34. package/dist/cart/store.js +189 -161
  35. package/dist/cart/store.js.map +1 -1
  36. package/dist/checkout/api/index.d.ts +6 -0
  37. package/dist/checkout/api/index.d.ts.map +1 -1
  38. package/dist/checkout/api/index.js +6 -0
  39. package/dist/checkout/api/index.js.map +1 -1
  40. package/dist/checkout/api/manual.d.ts.map +1 -1
  41. package/dist/checkout/api/manual.js +5 -1
  42. package/dist/checkout/api/manual.js.map +1 -1
  43. package/dist/checkout/api/payment-methods.d.ts +23 -6
  44. package/dist/checkout/api/payment-methods.d.ts.map +1 -1
  45. package/dist/checkout/api/payment-methods.js +67 -8
  46. package/dist/checkout/api/payment-methods.js.map +1 -1
  47. package/dist/checkout/api/paypal-capture.d.ts +38 -0
  48. package/dist/checkout/api/paypal-capture.d.ts.map +1 -0
  49. package/dist/checkout/api/paypal-capture.js +126 -0
  50. package/dist/checkout/api/paypal-capture.js.map +1 -0
  51. package/dist/checkout/api/paypal-webhook.d.ts +24 -0
  52. package/dist/checkout/api/paypal-webhook.d.ts.map +1 -0
  53. package/dist/checkout/api/paypal-webhook.js +122 -0
  54. package/dist/checkout/api/paypal-webhook.js.map +1 -0
  55. package/dist/checkout/api/paypal.d.ts +30 -0
  56. package/dist/checkout/api/paypal.d.ts.map +1 -0
  57. package/dist/checkout/api/paypal.js +70 -0
  58. package/dist/checkout/api/paypal.js.map +1 -0
  59. package/dist/checkout/api/up2pay-ipn.d.ts +28 -0
  60. package/dist/checkout/api/up2pay-ipn.d.ts.map +1 -0
  61. package/dist/checkout/api/up2pay-ipn.js +88 -0
  62. package/dist/checkout/api/up2pay-ipn.js.map +1 -0
  63. package/dist/checkout/api/up2pay.d.ts +34 -0
  64. package/dist/checkout/api/up2pay.d.ts.map +1 -0
  65. package/dist/checkout/api/up2pay.js +82 -0
  66. package/dist/checkout/api/up2pay.js.map +1 -0
  67. package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
  68. package/dist/checkout/components/CheckoutFlow.js +64 -9
  69. package/dist/checkout/components/CheckoutFlow.js.map +1 -1
  70. package/dist/checkout/components/PayPalCheckoutButtons.d.ts +32 -0
  71. package/dist/checkout/components/PayPalCheckoutButtons.d.ts.map +1 -0
  72. package/dist/checkout/components/PayPalCheckoutButtons.js +116 -0
  73. package/dist/checkout/components/PayPalCheckoutButtons.js.map +1 -0
  74. package/dist/checkout/components/index.d.ts +2 -0
  75. package/dist/checkout/components/index.d.ts.map +1 -1
  76. package/dist/checkout/components/index.js +1 -0
  77. package/dist/checkout/components/index.js.map +1 -1
  78. package/dist/checkout/components/payment-logos.d.ts +21 -0
  79. package/dist/checkout/components/payment-logos.d.ts.map +1 -0
  80. package/dist/checkout/components/payment-logos.js +21 -0
  81. package/dist/checkout/components/payment-logos.js.map +1 -0
  82. package/dist/checkout/hooks/useCheckout.d.ts.map +1 -1
  83. package/dist/checkout/hooks/useCheckout.js +160 -0
  84. package/dist/checkout/hooks/useCheckout.js.map +1 -1
  85. package/dist/checkout/server/confirm-order.d.ts +54 -0
  86. package/dist/checkout/server/confirm-order.d.ts.map +1 -0
  87. package/dist/checkout/server/confirm-order.js +147 -0
  88. package/dist/checkout/server/confirm-order.js.map +1 -0
  89. package/dist/checkout/server/external-checkout.d.ts +68 -0
  90. package/dist/checkout/server/external-checkout.d.ts.map +1 -0
  91. package/dist/checkout/server/external-checkout.js +187 -0
  92. package/dist/checkout/server/external-checkout.js.map +1 -0
  93. package/dist/checkout/server/paypal.d.ts +127 -0
  94. package/dist/checkout/server/paypal.d.ts.map +1 -0
  95. package/dist/checkout/server/paypal.js +274 -0
  96. package/dist/checkout/server/paypal.js.map +1 -0
  97. package/dist/checkout/server/up2pay.d.ts +152 -0
  98. package/dist/checkout/server/up2pay.d.ts.map +1 -0
  99. package/dist/checkout/server/up2pay.js +287 -0
  100. package/dist/checkout/server/up2pay.js.map +1 -0
  101. package/dist/checkout/types.d.ts +26 -1
  102. package/dist/checkout/types.d.ts.map +1 -1
  103. package/dist/core/db/queries.d.ts +68 -39
  104. package/dist/core/db/queries.d.ts.map +1 -1
  105. package/dist/core/db/queries.js +96 -0
  106. package/dist/core/db/queries.js.map +1 -1
  107. package/dist/core/db/schema.d.ts +210 -5
  108. package/dist/core/db/schema.d.ts.map +1 -1
  109. package/dist/core/db/schema.js +33 -0
  110. package/dist/core/db/schema.js.map +1 -1
  111. package/dist/core/i18n/messages/en.d.ts.map +1 -1
  112. package/dist/core/i18n/messages/en.js +6 -0
  113. package/dist/core/i18n/messages/en.js.map +1 -1
  114. package/dist/core/i18n/messages/fr.d.ts.map +1 -1
  115. package/dist/core/i18n/messages/fr.js +6 -0
  116. package/dist/core/i18n/messages/fr.js.map +1 -1
  117. package/docs/changelog/epoch-12-payment-providers.md +152 -0
  118. package/package.json +26 -2
  119. package/templates/store-template/.claude/skills/build-feature/SKILL.md +2 -1
  120. package/templates/store-template/app/api/admin/payments/paypal/route.ts +10 -0
  121. package/templates/store-template/app/api/admin/payments/up2pay/route.ts +10 -0
  122. package/templates/store-template/app/api/checkout/paypal/capture/route.ts +8 -0
  123. package/templates/store-template/app/api/checkout/paypal/return/route.ts +9 -0
  124. package/templates/store-template/app/api/checkout/paypal/route.ts +9 -0
  125. package/templates/store-template/app/api/checkout/paypal/webhook/route.ts +9 -0
  126. package/templates/store-template/app/api/checkout/up2pay/ipn/route.ts +8 -0
  127. package/templates/store-template/app/api/checkout/up2pay/route.ts +9 -0
  128. package/templates/store-template/package.json +2 -2
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @rovela/sdk/checkout/api/paypal
3
+ *
4
+ * POST /api/checkout/paypal — start a PayPal payment. Creates the order
5
+ * 'pending' (server-priced, §55) and a matching PayPal order (Orders v2,
6
+ * invoice_id = our reference). The client either renders the Smart Buttons
7
+ * popup (paypalOrderId) or redirects to approveUrl (fallback). Payment
8
+ * truth arrives via the capture leg (checkout/api/paypal-capture) with the
9
+ * webhook as async backstop — never via this route.
10
+ *
11
+ * Usage:
12
+ * ```typescript
13
+ * // app/api/checkout/paypal/route.ts
14
+ * export { POST } from '@rovela-ai/sdk/checkout/api/paypal'
15
+ * ```
16
+ */
17
+ import { NextResponse } from 'next/server';
18
+ import { preparePendingExternalOrder, resolveStoreBaseUrl, } from '../server/external-checkout';
19
+ import { resolvePaypalCredentials, createPaypalOrder, PaypalApiError } from '../server/paypal';
20
+ export const PAYPAL_METHOD_LABEL = 'PayPal';
21
+ export async function POST(request) {
22
+ try {
23
+ const body = (await request.json());
24
+ const prepared = await preparePendingExternalOrder({
25
+ body,
26
+ provider: 'paypal',
27
+ providerLabel: PAYPAL_METHOD_LABEL,
28
+ });
29
+ if (!prepared.ok)
30
+ return prepared.response;
31
+ const credentials = resolvePaypalCredentials(prepared.settings);
32
+ if (!credentials) {
33
+ return NextResponse.json({ error: 'This payment method is not available', code: 'INVALID_REQUEST' }, { status: 400 });
34
+ }
35
+ const base = resolveStoreBaseUrl(request.nextUrl.origin);
36
+ // Pickup/relay orders snapshot a merchant/relay address — PayPal must not
37
+ // see it as the buyer's shipping address, so only DELIVERY addresses are
38
+ // forwarded (SET_PROVIDED_ADDRESS); everything else is NO_SHIPPING.
39
+ const deliveryAddress = !prepared.quote.pickupLocationId && !prepared.quote.relay
40
+ ? prepared.orderShippingAddress
41
+ : null;
42
+ const paypalOrder = await createPaypalOrder({
43
+ credentials,
44
+ reference: prepared.reference,
45
+ totalCents: prepared.quote.totalCents,
46
+ currency: prepared.quote.currency,
47
+ storeName: prepared.settings?.storeName || 'Store',
48
+ returnUrl: `${base}/api/checkout/paypal/return`,
49
+ cancelUrl: `${base}/checkout?payment=cancelled`,
50
+ shippingAddress: deliveryAddress,
51
+ locale: prepared.orderLocale,
52
+ });
53
+ return NextResponse.json({
54
+ success: true,
55
+ orderId: prepared.orderId,
56
+ reference: prepared.reference,
57
+ paypalOrderId: paypalOrder.paypalOrderId,
58
+ approveUrl: paypalOrder.approveUrl,
59
+ });
60
+ }
61
+ catch (error) {
62
+ if (error instanceof PaypalApiError) {
63
+ console.error('[PayPal Checkout API] PayPal error:', error.status, error.issue, error.body);
64
+ return NextResponse.json({ error: 'PayPal could not start this payment. Please try again.', code: 'CHECKOUT_ERROR' }, { status: 502 });
65
+ }
66
+ console.error('[PayPal Checkout API] error:', error);
67
+ return NextResponse.json({ error: 'Failed to start the payment. Please try again.', code: 'CHECKOUT_ERROR' }, { status: 500 });
68
+ }
69
+ }
70
+ //# sourceMappingURL=paypal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paypal.js","sourceRoot":"","sources":["../../../src/checkout/api/paypal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @rovela/sdk/checkout/api/up2pay-ipn
3
+ *
4
+ * The Up2pay e-Transactions IPN (Instant Payment Notification) — the
5
+ * server-to-server callback that is the ONLY source of payment truth for
6
+ * Up2pay orders. Called by the platform for EVERY payment attempt
7
+ * (accepted or rejected), possibly multiple times and possibly BEFORE the
8
+ * customer's browser returns.
9
+ *
10
+ * Contract with the platform: respond 200 quickly, never redirect. The
11
+ * response body is a short plain-text marker (an empty page is
12
+ * discouraged by the integration manual).
13
+ *
14
+ * Field names follow UP2PAY_RETOUR (checkout/server/up2pay.ts):
15
+ * amount:M reference:R auto:A error:E transaction:S sign:K
16
+ *
17
+ * Usage:
18
+ * ```typescript
19
+ * // app/api/checkout/up2pay/ipn/route.ts
20
+ * export { GET, POST } from '@rovela-ai/sdk/checkout/api/up2pay-ipn'
21
+ * ```
22
+ */
23
+ import { NextRequest, NextResponse } from 'next/server';
24
+ /** Default IPN transport (no PBX_RUF1): params on the query string. */
25
+ export declare function GET(request: NextRequest): Promise<NextResponse>;
26
+ /** PBX_RUF1=POST variant: params in the form-encoded body. */
27
+ export declare function POST(request: NextRequest): Promise<NextResponse>;
28
+ //# sourceMappingURL=up2pay-ipn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"up2pay-ipn.d.ts","sourceRoot":"","sources":["../../../src/checkout/api/up2pay-ipn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAsEvD,uEAAuE;AACvE,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAErE;AAED,8DAA8D;AAC9D,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAGtE"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @rovela/sdk/checkout/api/up2pay-ipn
3
+ *
4
+ * The Up2pay e-Transactions IPN (Instant Payment Notification) — the
5
+ * server-to-server callback that is the ONLY source of payment truth for
6
+ * Up2pay orders. Called by the platform for EVERY payment attempt
7
+ * (accepted or rejected), possibly multiple times and possibly BEFORE the
8
+ * customer's browser returns.
9
+ *
10
+ * Contract with the platform: respond 200 quickly, never redirect. The
11
+ * response body is a short plain-text marker (an empty page is
12
+ * discouraged by the integration manual).
13
+ *
14
+ * Field names follow UP2PAY_RETOUR (checkout/server/up2pay.ts):
15
+ * amount:M reference:R auto:A error:E transaction:S sign:K
16
+ *
17
+ * Usage:
18
+ * ```typescript
19
+ * // app/api/checkout/up2pay/ipn/route.ts
20
+ * export { GET, POST } from '@rovela-ai/sdk/checkout/api/up2pay-ipn'
21
+ * ```
22
+ */
23
+ import { NextResponse } from 'next/server';
24
+ import { verifyUp2payIpnSignature } from '../server/up2pay';
25
+ import { confirmExternalOrderPaid } from '../server/confirm-order';
26
+ const ok = (body) => new NextResponse(body, { status: 200, headers: { 'Content-Type': 'text/plain' } });
27
+ async function handleIpn(raw) {
28
+ try {
29
+ if (!raw || raw === '?') {
30
+ return new NextResponse('missing parameters', { status: 400 });
31
+ }
32
+ const { valid, fields } = verifyUp2payIpnSignature(raw);
33
+ const reference = fields.reference ?? '';
34
+ const errorCode = fields.error ?? '';
35
+ if (!valid) {
36
+ // A failed signature on a money callback is either misconfiguration or
37
+ // tampering — log loudly, process NOTHING. 400 (not 200) so a platform
38
+ // retry after a config fix can still land.
39
+ console.error(`[Up2pay IPN] SIGNATURE VERIFICATION FAILED — ref=${reference} error=${errorCode} (nothing processed)`);
40
+ return new NextResponse('signature verification failed', { status: 400 });
41
+ }
42
+ if (!reference.startsWith('up2pay_')) {
43
+ console.error(`[Up2pay IPN] unexpected reference shape: ${reference}`);
44
+ return ok('ignored');
45
+ }
46
+ // Rejected / abandoned attempt: the customer may retry from the hosted
47
+ // page — the order stays pending (it reads as an unpaid attempt in
48
+ // admin, same as an unpaid manual order). Log for the merchant trail.
49
+ if (errorCode !== '00000' || !fields.auto) {
50
+ console.warn(`[Up2pay IPN] non-success attempt ref=${reference} error=${errorCode} auto=${fields.auto ?? ''}`);
51
+ return ok('acknowledged');
52
+ }
53
+ const paidAmountCents = Number.parseInt(fields.amount ?? '', 10);
54
+ if (!Number.isInteger(paidAmountCents) || paidAmountCents <= 0) {
55
+ console.error(`[Up2pay IPN] invalid amount for ${reference}: ${fields.amount}`);
56
+ return ok('acknowledged');
57
+ }
58
+ const result = await confirmExternalOrderPaid({
59
+ reference,
60
+ expectedProvider: 'up2pay',
61
+ providerLabel: 'Up2pay e-Transactions',
62
+ paidAmountCents,
63
+ transaction: [
64
+ ...(fields.transaction
65
+ ? [{ key: 'up2pay_transaction', label: 'Up2pay transaction', value: fields.transaction }]
66
+ : []),
67
+ { key: 'up2pay_authorization', label: 'Authorization number', value: fields.auto },
68
+ ],
69
+ });
70
+ console.log(`[Up2pay IPN] ref=${reference} → ${result.status}`);
71
+ return ok(result.status === 'amount_mismatch' ? 'amount mismatch' : 'ok');
72
+ }
73
+ catch (error) {
74
+ console.error('[Up2pay IPN] error:', error);
75
+ // 500 → the platform may retry the notification.
76
+ return new NextResponse('error', { status: 500 });
77
+ }
78
+ }
79
+ /** Default IPN transport (no PBX_RUF1): params on the query string. */
80
+ export async function GET(request) {
81
+ return handleIpn(request.nextUrl.search);
82
+ }
83
+ /** PBX_RUF1=POST variant: params in the form-encoded body. */
84
+ export async function POST(request) {
85
+ const raw = await request.text();
86
+ return handleIpn(raw);
87
+ }
88
+ //# sourceMappingURL=up2pay-ipn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"up2pay-ipn.js","sourceRoot":"","sources":["../../../src/checkout/api/up2pay-ipn.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @rovela/sdk/checkout/api/up2pay
3
+ *
4
+ * POST /api/checkout/up2pay — start an Up2pay e-Transactions (Crédit
5
+ * Agricole) hosted-page payment. Creates the order 'pending' (server-priced,
6
+ * §55) and returns the HMAC-signed form the browser auto-submits to the
7
+ * hosted payment page. The IPN (checkout/api/up2pay-ipn) is the ONLY thing
8
+ * that ever flips the order to paid.
9
+ *
10
+ * Usage:
11
+ * ```typescript
12
+ * // app/api/checkout/up2pay/route.ts
13
+ * export { POST } from '@rovela-ai/sdk/checkout/api/up2pay'
14
+ * ```
15
+ */
16
+ import { NextRequest, NextResponse } from 'next/server';
17
+ import type { CheckoutApiError } from '../types';
18
+ export declare const UP2PAY_METHOD_LABEL = "Up2pay e-Transactions";
19
+ interface Up2payCheckoutResponse {
20
+ success: true;
21
+ orderId: string;
22
+ reference: string;
23
+ /** The hosted payment page to POST to. */
24
+ url: string;
25
+ method: 'POST';
26
+ /** Hidden form fields IN ORDER (the HMAC covers this exact order). */
27
+ fields: Array<{
28
+ name: string;
29
+ value: string;
30
+ }>;
31
+ }
32
+ export declare function POST(request: NextRequest): Promise<NextResponse<Up2payCheckoutResponse | CheckoutApiError>>;
33
+ export {};
34
+ //# sourceMappingURL=up2pay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"up2pay.d.ts","sourceRoot":"","sources":["../../../src/checkout/api/up2pay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAWvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAEhD,eAAO,MAAM,mBAAmB,0BAA0B,CAAA;AAE1D,UAAU,sBAAsB;IAC9B,OAAO,EAAE,IAAI,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/C;AAED,wBAAsB,IAAI,CACxB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,YAAY,CAAC,sBAAsB,GAAG,gBAAgB,CAAC,CAAC,CAyElE"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @rovela/sdk/checkout/api/up2pay
3
+ *
4
+ * POST /api/checkout/up2pay — start an Up2pay e-Transactions (Crédit
5
+ * Agricole) hosted-page payment. Creates the order 'pending' (server-priced,
6
+ * §55) and returns the HMAC-signed form the browser auto-submits to the
7
+ * hosted payment page. The IPN (checkout/api/up2pay-ipn) is the ONLY thing
8
+ * that ever flips the order to paid.
9
+ *
10
+ * Usage:
11
+ * ```typescript
12
+ * // app/api/checkout/up2pay/route.ts
13
+ * export { POST } from '@rovela-ai/sdk/checkout/api/up2pay'
14
+ * ```
15
+ */
16
+ import { NextResponse } from 'next/server';
17
+ import { preparePendingExternalOrder, resolveStoreBaseUrl, } from '../server/external-checkout';
18
+ import { resolveUp2payCredentials, buildUp2payRedirect, Up2payError, } from '../server/up2pay';
19
+ export const UP2PAY_METHOD_LABEL = 'Up2pay e-Transactions';
20
+ export async function POST(request) {
21
+ try {
22
+ const body = (await request.json());
23
+ const prepared = await preparePendingExternalOrder({
24
+ body,
25
+ provider: 'up2pay',
26
+ providerLabel: UP2PAY_METHOD_LABEL,
27
+ });
28
+ if (!prepared.ok)
29
+ return prepared.response;
30
+ const credentials = resolveUp2payCredentials(prepared.settings);
31
+ if (!credentials) {
32
+ return NextResponse.json({ error: 'This payment method is not available', code: 'INVALID_REQUEST' }, { status: 400 });
33
+ }
34
+ const base = resolveStoreBaseUrl(request.nextUrl.origin);
35
+ const billingSource = prepared.orderBillingAddress ?? body.shippingAddress ?? prepared.orderShippingAddress;
36
+ let form;
37
+ try {
38
+ form = buildUp2payRedirect({
39
+ credentials,
40
+ reference: prepared.reference,
41
+ totalCents: prepared.quote.totalCents,
42
+ currency: prepared.quote.currency,
43
+ customerEmail: prepared.email,
44
+ billing: {
45
+ firstName: billingSource?.firstName || prepared.email.split('@')[0] || 'Client',
46
+ lastName: billingSource?.lastName || 'Client',
47
+ line1: billingSource?.line1 || '-',
48
+ line2: billingSource?.line2 ?? null,
49
+ postalCode: billingSource?.postalCode ?? null,
50
+ city: billingSource?.city || '-',
51
+ country: billingSource?.country || prepared.settings?.storeCountry || 'FR',
52
+ },
53
+ totalQuantity: prepared.quote.lines.reduce((sum, l) => sum + (l.quantity || 0), 0),
54
+ effectueUrl: `${base}/checkout/success?orderId=${encodeURIComponent(prepared.orderId)}`,
55
+ refuseUrl: `${base}/checkout?payment=refused`,
56
+ annuleUrl: `${base}/checkout?payment=cancelled`,
57
+ attenteUrl: `${base}/checkout/success?orderId=${encodeURIComponent(prepared.orderId)}`,
58
+ ipnUrl: `${base}/api/checkout/up2pay/ipn`,
59
+ locale: prepared.orderLocale,
60
+ });
61
+ }
62
+ catch (err) {
63
+ if (err instanceof Up2payError) {
64
+ return NextResponse.json({ error: err.message, code: 'INVALID_REQUEST' }, { status: 400 });
65
+ }
66
+ throw err;
67
+ }
68
+ return NextResponse.json({
69
+ success: true,
70
+ orderId: prepared.orderId,
71
+ reference: prepared.reference,
72
+ url: form.url,
73
+ method: form.method,
74
+ fields: form.fields,
75
+ });
76
+ }
77
+ catch (error) {
78
+ console.error('[Up2pay Checkout API] error:', error);
79
+ return NextResponse.json({ error: 'Failed to start the payment. Please try again.', code: 'CHECKOUT_ERROR' }, { status: 500 });
80
+ }
81
+ }
82
+ //# sourceMappingURL=up2pay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"up2pay.js","sourceRoot":"","sources":["../../../src/checkout/api/up2pay.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CheckoutFlow.d.ts","sourceRoot":"","sources":["../../../src/checkout/components/CheckoutFlow.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAoBH,OAAO,KAAK,EACV,iBAAiB,EAOlB,MAAM,UAAU,CAAA;AA+UjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,QAAQ,EACR,aAAa,EACb,UAAU,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,gBAAuB,EACvB,QAAgB,EAChB,QAAQ,GACT,EAAE,iBAAiB,2CA80CnB"}
1
+ {"version":3,"file":"CheckoutFlow.d.ts","sourceRoot":"","sources":["../../../src/checkout/components/CheckoutFlow.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAsBH,OAAO,KAAK,EACV,iBAAiB,EAOlB,MAAM,UAAU,CAAA;AA+UjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,QAAQ,EACR,aAAa,EACb,UAAU,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,gBAAuB,EACvB,QAAgB,EAChB,QAAQ,GACT,EAAE,iBAAiB,2CAk8CnB"}
@@ -26,6 +26,8 @@ import { SignUpForm } from '../../auth/components/SignUpForm';
26
26
  import { ShippingForm } from './ShippingForm';
27
27
  import { ShippingOptions } from './ShippingOptions';
28
28
  import { OrderSummary } from './OrderSummary';
29
+ import { PayPalCheckoutButtons } from './PayPalCheckoutButtons';
30
+ import { PAYPAL_LOGO_DATA_URI, CREDIT_AGRICOLE_LOGO_DATA_URI } from './payment-logos';
29
31
  /**
30
32
  * Client-side French mobile check (relay UX pre-validation only — the pricing
31
33
  * core re-validates server-side; see resolveShippingSelection).
@@ -266,10 +268,20 @@ export function CheckoutFlow({ onComplete, onCancel, customerEmail, customerId,
266
268
  // Never gate when accounts are Off (that combo is normalized away, but a
267
269
  // hand-edited row must not strand shoppers behind a sign-in that can't exist).
268
270
  const showAuthGate = storeSettings?.guestCheckout === false && storeSettings?.customerAccounts !== false && !customerId;
269
- // Payment methods beyond native Stripe (cash on delivery, wire…). Empty list
270
- // the selector never renders and checkout is byte-identical to before.
271
+ // Payment methods beyond native Stripe: registry rows (cash on delivery,
272
+ // wire…) plus synthetic external-provider entries (PayPal, Up2pay SDK
273
+ // 0.25.0). Empty list → the selector never renders and checkout is
274
+ // byte-identical to before.
271
275
  const [paymentMethods, setPaymentMethods] = useState([]);
272
276
  const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState(''); // '' = card via Stripe
277
+ // Whether native Stripe card payment exists on THIS store (0.25.1). The
278
+ // server reports it from the same env truth the session route runs on —
279
+ // the card option must NEVER render on a store without Stripe (shoppers
280
+ // were offered a dead payment path). Defaults true so Stripe stores keep
281
+ // a flash-free first paint; the fetch corrects it long before the review
282
+ // step is reachable.
283
+ const [stripeAvailable, setStripeAvailable] = useState(true);
284
+ const selectedPaymentMethod = paymentMethods.find((m) => m.id === selectedPaymentMethodId) ?? null;
273
285
  // Pattern C — populate state from async-loaded data.
274
286
  useEffect(() => {
275
287
  let active = true;
@@ -278,6 +290,14 @@ export function CheckoutFlow({ onComplete, onCancel, customerEmail, customerId,
278
290
  .then((data) => {
279
291
  if (active && data?.success && Array.isArray(data.methods)) {
280
292
  setPaymentMethods(data.methods);
293
+ const hasStripe = data.stripeAvailable !== false;
294
+ setStripeAvailable(hasStripe);
295
+ // No Stripe → '' (the card default) is not a valid selection;
296
+ // preselect the first REAL option so the order button always maps
297
+ // to an available payment path.
298
+ if (!hasStripe) {
299
+ setSelectedPaymentMethodId((prev) => prev || (data.methods[0]?.id ?? ''));
300
+ }
281
301
  }
282
302
  })
283
303
  .catch(() => {
@@ -352,7 +372,7 @@ export function CheckoutFlow({ onComplete, onCancel, customerEmail, customerId,
352
372
  // eslint-disable-next-line react-hooks/exhaustive-deps
353
373
  }, []);
354
374
  // Checkout hook with shipping params
355
- const { checkout, checkoutManual, isLoading: isCheckoutLoading, error: checkoutError } = useCheckout({
375
+ const { checkout, checkoutManual, checkoutUp2pay, createPaypalOrder, capturePaypalOrder, isLoading: isCheckoutLoading, error: checkoutError, } = useCheckout({
356
376
  customerEmail: state.email || undefined,
357
377
  customerId,
358
378
  successUrl,
@@ -696,13 +716,42 @@ export function CheckoutFlow({ onComplete, onCancel, customerEmail, customerId,
696
716
  currency: resolvedCurrency,
697
717
  path: typeof window !== 'undefined' ? window.location.pathname : '/checkout',
698
718
  });
699
- if (selectedPaymentMethodId) {
719
+ if (selectedPaymentMethod?.provider === 'up2pay') {
720
+ // External redirect provider — hosted payment page, IPN-confirmed.
721
+ await checkoutUp2pay();
722
+ }
723
+ else if (selectedPaymentMethodId && selectedPaymentMethod?.kind !== 'external') {
700
724
  await checkoutManual(selectedPaymentMethodId);
701
725
  }
702
- else {
726
+ else if (stripeAvailable) {
703
727
  await checkout();
704
728
  }
705
- }, [checkout, checkoutManual, selectedPaymentMethodId, subtotal, state.selectedShipping, state.taxIncludedInPrices, state.tax, resolvedCurrency]);
729
+ else {
730
+ // Defense in depth — the UI never offers the card path without
731
+ // Stripe, so reaching here means a stale selection.
732
+ setState((prev) => ({ ...prev, isProcessing: false }));
733
+ }
734
+ }, [checkout, checkoutManual, checkoutUp2pay, selectedPaymentMethod, selectedPaymentMethodId, stripeAvailable, subtotal, state.selectedShipping, state.taxIncludedInPrices, state.tax, resolvedCurrency]);
735
+ // PayPal Smart Buttons legs (the buttons replace the main checkout button
736
+ // when the PayPal provider is selected — approval happens in the popup,
737
+ // capture on the server).
738
+ const handlePaypalCreateOrder = useCallback(async () => {
739
+ const shippingCents = state.selectedShipping?.price || 0;
740
+ const subtotalC = Math.round(subtotal * 100);
741
+ const totalC = state.taxIncludedInPrices
742
+ ? subtotalC + shippingCents
743
+ : subtotalC + shippingCents + (state.tax || 0);
744
+ emitEvent({
745
+ event: 'begin_checkout',
746
+ value_cents: totalC,
747
+ currency: resolvedCurrency,
748
+ path: typeof window !== 'undefined' ? window.location.pathname : '/checkout',
749
+ });
750
+ return createPaypalOrder();
751
+ }, [createPaypalOrder, subtotal, state.selectedShipping, state.taxIncludedInPrices, state.tax, resolvedCurrency]);
752
+ const handlePaypalApprove = useCallback(async (paypalOrderId) => {
753
+ await capturePaypalOrder(paypalOrderId);
754
+ }, [capturePaypalOrder]);
706
755
  // Go back to previous step
707
756
  const goBack = useCallback(() => {
708
757
  setState((prev) => {
@@ -813,9 +862,15 @@ export function CheckoutFlow({ onComplete, onCancel, customerEmail, customerId,
813
862
  .filter(Boolean)
814
863
  .join(', ') })] })] }), formatRelayDistance(relay.distanceMeters) && (_jsx("span", { className: "text-xs text-muted-foreground whitespace-nowrap", children: formatRelayDistance(relay.distanceMeters) }))] }, relay.id))) }), relayPoints.length > 5 && !showAllRelays && (_jsx("button", { type: "button", onClick: () => setShowAllRelays(true), className: "text-sm underline text-muted-foreground hover:text-foreground", children: t('checkout.relayShowMore', { count: relayPoints.length - 5 }) }))] }))] })) : null }), _jsxs("div", { className: "flex gap-4 pt-4", children: [_jsx("button", { type: "button", onClick: goBack, className: cn('flex-1 px-4 py-3 rounded-md font-medium', 'border border-input bg-background', 'hover:bg-muted', 'transition-colors'), children: t('common.back') }), _jsx("button", { type: "button", onClick: handleShippingContinue, disabled: !state.selectedShipping ||
815
864
  state.isLoadingShipping ||
816
- (isRelaySelected && (!selectedRelay || !relayMobileOk)), className: cn('flex-1 px-4 py-3 rounded-md font-medium', 'bg-primary text-primary-foreground', 'hover:bg-primary/90', 'transition-colors', 'disabled:opacity-50 disabled:cursor-not-allowed'), children: t('checkout.continueToReview') })] })] })), state.step === 'review' && state.shippingAddress && state.selectedShipping && (_jsxs("div", { className: "space-y-6", children: [_jsx("h2", { className: "text-xl font-semibold", children: t('checkout.reviewYourOrder') }), isPickup && state.selectedPickupLocation ? (_jsx("div", { className: "space-y-4", children: _jsxs("div", { className: "p-4 border rounded-lg space-y-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("h4", { className: "flex items-center gap-2 font-medium", children: [_jsx(Store, { className: "h-4 w-4" }), t('checkout.pickupInStore')] }), _jsx("button", { type: "button", onClick: () => setState((prev) => ({ ...prev, step: 'address' })), className: "text-sm text-primary hover:underline shrink-0", children: t('common.edit') })] }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-medium", children: state.selectedPickupLocation.name }), _jsx("p", { className: "text-muted-foreground", children: pickupAddressLine(state.selectedPickupLocation) }), formatTodayHours(state.selectedPickupLocation, t) && (_jsxs("p", { className: "text-muted-foreground flex items-center gap-1 mt-1", children: [_jsx(Clock, { className: "h-3 w-3" }), formatTodayHours(state.selectedPickupLocation, t)] })), state.selectedPickupLocation.preparationTime && (_jsx("p", { className: "text-emerald-600 mt-1", children: t('checkout.usuallyReadyIn', { time: translateDynamic(t, 'prep', state.selectedPickupLocation.preparationTime) }) }))] }), _jsxs("div", { className: "border-t pt-3 text-sm", children: [_jsxs("p", { className: "font-medium", children: [pickupContact.firstName, " ", pickupContact.lastName] }), _jsx("p", { className: "text-muted-foreground", children: state.email || customerEmail || '' })] })] }) })) : (_jsx(ReviewSection, { shippingAddress: state.shippingAddress, billingAddress: state.billingAddress, selectedShipping: state.selectedShipping, email: state.email || customerEmail || '', currency: resolvedCurrency, onEditAddress: () => setState((prev) => ({ ...prev, step: 'address' })), onEditShipping: () => setState((prev) => ({ ...prev, step: 'shipping' })) })), !showOrderSummary && discountBlock, paymentMethods.length > 0 && (_jsxs("div", { className: "p-4 border rounded-lg", children: [_jsx("h4", { className: "font-medium mb-3", children: t('checkout.paymentMethod') }), _jsxs("div", { className: "space-y-2", children: [_jsxs("label", { className: cn('flex items-start gap-3 rounded-md border p-3 cursor-pointer transition-colors', selectedPaymentMethodId === '' && 'border-primary bg-primary/5'), children: [_jsx("input", { type: "radio", name: "payment-method", checked: selectedPaymentMethodId === '', onChange: () => setSelectedPaymentMethodId(''), className: "mt-1" }), _jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: t('checkout.card') }), _jsx("span", { className: "block text-xs text-muted-foreground", children: t('checkout.secureViaStripe') })] })] }), paymentMethods.map((m) => (_jsxs("label", { className: cn('flex items-start gap-3 rounded-md border p-3 cursor-pointer transition-colors', selectedPaymentMethodId === m.id && 'border-primary bg-primary/5'), children: [_jsx("input", { type: "radio", name: "payment-method", checked: selectedPaymentMethodId === m.id, onChange: () => setSelectedPaymentMethodId(m.id), className: "mt-1" }), _jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: m.name }), m.instructions && (_jsx("span", { className: "block text-xs text-muted-foreground line-clamp-2", children: m.instructions }))] })] }, m.id)))] })] })), _jsx("button", { type: "button", onClick: handleCheckout, disabled: state.isProcessing || isCheckoutLoading, className: cn('w-full px-4 py-4 rounded-md font-medium text-lg', 'bg-primary text-primary-foreground', 'hover:bg-primary/90', 'transition-colors', 'disabled:opacity-50 disabled:cursor-not-allowed', 'flex items-center justify-center gap-2'), children: (state.isProcessing || isCheckoutLoading) ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-5 w-5 animate-spin" }), t('common.processing')] })) : (_jsxs(_Fragment, { children: [_jsx(CreditCard, { className: "h-5 w-5" }), selectedPaymentMethodId ? t('checkout.placeOrder') : t('checkout.proceedToPayment')] })) }), _jsx("p", { className: "text-xs text-center text-muted-foreground", children: selectedPaymentMethodId
817
- ? t('checkout.manualConfirmNote')
818
- : t('checkout.stripeRedirectNote') })] }))] })) }), showOrderSummary && (_jsx("div", { className: "lg:col-span-1", children: _jsxs("div", { className: "sticky top-4 space-y-3", children: [_jsx(OrderSummary, { items: items.map((item) => ({
865
+ (isRelaySelected && (!selectedRelay || !relayMobileOk)), className: cn('flex-1 px-4 py-3 rounded-md font-medium', 'bg-primary text-primary-foreground', 'hover:bg-primary/90', 'transition-colors', 'disabled:opacity-50 disabled:cursor-not-allowed'), children: t('checkout.continueToReview') })] })] })), state.step === 'review' && state.shippingAddress && state.selectedShipping && (_jsxs("div", { className: "space-y-6", children: [_jsx("h2", { className: "text-xl font-semibold", children: t('checkout.reviewYourOrder') }), isPickup && state.selectedPickupLocation ? (_jsx("div", { className: "space-y-4", children: _jsxs("div", { className: "p-4 border rounded-lg space-y-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("h4", { className: "flex items-center gap-2 font-medium", children: [_jsx(Store, { className: "h-4 w-4" }), t('checkout.pickupInStore')] }), _jsx("button", { type: "button", onClick: () => setState((prev) => ({ ...prev, step: 'address' })), className: "text-sm text-primary hover:underline shrink-0", children: t('common.edit') })] }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-medium", children: state.selectedPickupLocation.name }), _jsx("p", { className: "text-muted-foreground", children: pickupAddressLine(state.selectedPickupLocation) }), formatTodayHours(state.selectedPickupLocation, t) && (_jsxs("p", { className: "text-muted-foreground flex items-center gap-1 mt-1", children: [_jsx(Clock, { className: "h-3 w-3" }), formatTodayHours(state.selectedPickupLocation, t)] })), state.selectedPickupLocation.preparationTime && (_jsx("p", { className: "text-emerald-600 mt-1", children: t('checkout.usuallyReadyIn', { time: translateDynamic(t, 'prep', state.selectedPickupLocation.preparationTime) }) }))] }), _jsxs("div", { className: "border-t pt-3 text-sm", children: [_jsxs("p", { className: "font-medium", children: [pickupContact.firstName, " ", pickupContact.lastName] }), _jsx("p", { className: "text-muted-foreground", children: state.email || customerEmail || '' })] })] }) })) : (_jsx(ReviewSection, { shippingAddress: state.shippingAddress, billingAddress: state.billingAddress, selectedShipping: state.selectedShipping, email: state.email || customerEmail || '', currency: resolvedCurrency, onEditAddress: () => setState((prev) => ({ ...prev, step: 'address' })), onEditShipping: () => setState((prev) => ({ ...prev, step: 'shipping' })) })), !showOrderSummary && discountBlock, paymentMethods.length > 0 && (_jsxs("div", { className: "p-4 border rounded-lg", children: [_jsx("h4", { className: "font-medium mb-3", children: t('checkout.paymentMethod') }), _jsxs("div", { className: "space-y-2", children: [stripeAvailable && (_jsxs("label", { className: cn('flex items-start gap-3 rounded-md border p-3 cursor-pointer transition-colors', selectedPaymentMethodId === '' && 'border-primary bg-primary/5'), children: [_jsx("input", { type: "radio", name: "payment-method", checked: selectedPaymentMethodId === '', onChange: () => setSelectedPaymentMethodId(''), className: "mt-1" }), _jsxs("span", { className: "flex-1", children: [_jsx("span", { className: "block text-sm font-medium", children: t('checkout.card') }), _jsx("span", { className: "block text-xs text-muted-foreground", children: t('checkout.secureViaStripe') })] }), _jsx(CreditCard, { className: "h-5 w-5 mt-0.5 text-muted-foreground", "aria-hidden": true })] })), paymentMethods.map((m) => (_jsxs("label", { className: cn('flex items-start gap-3 rounded-md border p-3 cursor-pointer transition-colors', selectedPaymentMethodId === m.id && 'border-primary bg-primary/5'), children: [_jsx("input", { type: "radio", name: "payment-method", checked: selectedPaymentMethodId === m.id, onChange: () => setSelectedPaymentMethodId(m.id), className: "mt-1" }), _jsxs("span", { className: "flex-1", children: [_jsx("span", { className: "block text-sm font-medium", children: m.provider === 'up2pay' ? t('checkout.up2payLabel') : m.name }), m.provider === 'paypal' && (_jsx("span", { className: "block text-xs text-muted-foreground", children: t('checkout.paypalNote') })), m.provider === 'up2pay' && (_jsx("span", { className: "block text-xs text-muted-foreground", children: t('checkout.up2payNote') })), m.instructions && (_jsx("span", { className: "block text-xs text-muted-foreground line-clamp-2", children: m.instructions }))] }), m.provider === 'paypal' && (
866
+ // eslint-disable-next-line @next/next/no-img-element
867
+ _jsx("img", { src: PAYPAL_LOGO_DATA_URI, alt: "", className: "h-4 mt-1" })), m.provider === 'up2pay' && (
868
+ // eslint-disable-next-line @next/next/no-img-element
869
+ _jsx("img", { src: CREDIT_AGRICOLE_LOGO_DATA_URI, alt: "", className: "h-6 mt-0.5" }))] }, m.id)))] })] })), !stripeAvailable && paymentMethods.length === 0 ? (_jsx("div", { className: "p-4 border rounded-lg bg-muted/30 text-center text-sm text-muted-foreground", children: t('checkout.noPaymentMethods') })) : selectedPaymentMethod?.provider === 'paypal' && selectedPaymentMethod.paypal ? (_jsx(PayPalCheckoutButtons, { clientId: selectedPaymentMethod.paypal.clientId, currency: resolvedCurrency, disabled: state.isProcessing || isCheckoutLoading, createOrder: handlePaypalCreateOrder, onApprove: handlePaypalApprove })) : (_jsx("button", { type: "button", onClick: handleCheckout, disabled: state.isProcessing || isCheckoutLoading, className: cn('w-full px-4 py-4 rounded-md font-medium text-lg', 'bg-primary text-primary-foreground', 'hover:bg-primary/90', 'transition-colors', 'disabled:opacity-50 disabled:cursor-not-allowed', 'flex items-center justify-center gap-2'), children: (state.isProcessing || isCheckoutLoading) ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-5 w-5 animate-spin" }), t('common.processing')] })) : (_jsxs(_Fragment, { children: [_jsx(CreditCard, { className: "h-5 w-5" }), selectedPaymentMethodId ? t('checkout.placeOrder') : t('checkout.proceedToPayment')] })) })), (stripeAvailable || paymentMethods.length > 0) && (_jsx("p", { className: "text-xs text-center text-muted-foreground", children: selectedPaymentMethod?.kind === 'external'
870
+ ? t('checkout.pspRedirectNote')
871
+ : selectedPaymentMethodId
872
+ ? t('checkout.manualConfirmNote')
873
+ : t('checkout.stripeRedirectNote') }))] }))] })) }), showOrderSummary && (_jsx("div", { className: "lg:col-span-1", children: _jsxs("div", { className: "sticky top-4 space-y-3", children: [_jsx(OrderSummary, { items: items.map((item) => ({
819
874
  productId: item.productId,
820
875
  variantId: item.variantId,
821
876
  name: item.name,