@pandait.tech/payment-nuvei 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,9 +141,44 @@ A production-validated implementation of both (`threeDSCallback` + `nuveiProxy`,
141
141
 
142
142
  ## UI components — usage
143
143
 
144
- All `/ui` exports are client components (`"use client"`) and expect a Tailwind setup. The components consume CSS variables for theming so the same components work for every white-label client with their own branding.
144
+ All `/ui` exports are client components (`"use client"`). They consume CSS variables for theming so the same components work for every white-label client with their own branding.
145
145
 
146
- ### 1. Define CSS variables in your global stylesheet
146
+ ### 1. Load the styles
147
+
148
+ You have two options. **Option A is recommended** — it works without Tailwind, on any Tailwind version, and doesn't require configuring your build to scan this package.
149
+
150
+ **Option A — import the prebuilt stylesheet (recommended, standalone):**
151
+
152
+ ```ts
153
+ // once, e.g. in app/layout.tsx or your global entry
154
+ import "@pandait.tech/payment-nuvei/ui/styles.css";
155
+ ```
156
+
157
+ This ships the exact utility classes the components use (no Tailwind Preflight/reset — it won't touch your page's base styles). You still define the brand tokens (step 2) for theming.
158
+
159
+ **Option B — let your own Tailwind build the classes (Tailwind v4 users):**
160
+
161
+ Skip the CSS import and instead point Tailwind at the package so it generates the classes itself. Tailwind **v4** (`@source` in your CSS):
162
+
163
+ ```css
164
+ @import "tailwindcss";
165
+ @source "../node_modules/@pandait.tech/payment-nuvei/dist/**/*.{js,cjs}";
166
+ ```
167
+
168
+ Tailwind **v3** (`content` in `tailwind.config`):
169
+
170
+ ```js
171
+ export default {
172
+ content: [
173
+ "./app/**/*.{ts,tsx}",
174
+ "./node_modules/@pandait.tech/payment-nuvei/dist/**/*.{js,cjs}",
175
+ ],
176
+ };
177
+ ```
178
+
179
+ > If components render unstyled, you forgot Option A's import or Option B's `@source`/`content` entry. Option A removes that failure mode entirely.
180
+
181
+ ### 2. Define CSS variables in your global stylesheet
147
182
 
148
183
  Add this to your `app/globals.css` (Next.js) or equivalent, in `:root`:
149
184
 
@@ -177,22 +212,6 @@ Add this to your `app/globals.css` (Next.js) or equivalent, in `:root`:
177
212
  }
178
213
  ```
179
214
 
180
- ### 2. Add the package to Tailwind's content config
181
-
182
- Tailwind needs to detect the arbitrary-value utility classes (e.g. `bg-[var(--color-primary)]`) inside the package's compiled output:
183
-
184
- ```js
185
- // tailwind.config.ts
186
- export default {
187
- content: [
188
- "./app/**/*.{ts,tsx}",
189
- "./src/**/*.{ts,tsx}",
190
- "./node_modules/@pandait.tech/payment-nuvei/dist/**/*.{js,cjs}",
191
- ],
192
- // ...
193
- };
194
- ```
195
-
196
215
  ### 3. Use the components
197
216
 
198
217
  ```tsx
@@ -922,7 +922,43 @@ function createWebhookHandler(deps) {
922
922
  }
923
923
 
924
924
  // src/handlers/3ds-callback.ts
925
+ function pickCres(src) {
926
+ for (const key of Object.keys(src)) {
927
+ const k = key.toLowerCase();
928
+ if (k === "cres" || k === "cres_value" || k === "cresvalue" || k === "value" || k === "param.value" || k === "paramvalue") {
929
+ const v = src[key];
930
+ if (typeof v === "string" && v.length > 0) return v;
931
+ }
932
+ }
933
+ return src.cres || src.CRes || src.CRES || src.value || "";
934
+ }
935
+ function transStatusFromCres(cres, logger) {
936
+ try {
937
+ const base64 = cres.replace(/-/g, "+").replace(/_/g, "/");
938
+ const pad = base64.length % 4;
939
+ const padded = pad ? base64 + "=".repeat(4 - pad) : base64;
940
+ const decoded = JSON.parse(
941
+ Buffer.from(padded, "base64").toString("utf-8")
942
+ );
943
+ if (decoded && typeof decoded.transStatus === "string") {
944
+ return decoded.transStatus;
945
+ }
946
+ } catch (e) {
947
+ logger.warn(
948
+ "[3ds-callback] Could not decode CRES payload:",
949
+ e instanceof Error ? e.message : e
950
+ );
951
+ }
952
+ return null;
953
+ }
954
+ function paramsToObject(params) {
955
+ const obj = {};
956
+ for (const [k, v] of params.entries()) obj[k] = v;
957
+ return obj;
958
+ }
925
959
  function buildCompletionPage(orderId, transStatus) {
960
+ const safeOrderId = String(orderId).replace(/[^a-zA-Z0-9_-]/g, "");
961
+ const safeTransStatus = String(transStatus).replace(/[^a-zA-Z0-9]/g, "");
926
962
  const html = `<!DOCTYPE html>
927
963
  <html><head><meta charset="utf-8"><title>3DS Verification</title></head>
928
964
  <body>
@@ -930,8 +966,8 @@ function buildCompletionPage(orderId, transStatus) {
930
966
  (function() {
931
967
  var message = {
932
968
  type: "3DS_COMPLETE",
933
- orderId: "${orderId}",
934
- transStatus: "${transStatus}"
969
+ orderId: "${safeOrderId}",
970
+ transStatus: "${safeTransStatus}"
935
971
  };
936
972
  try {
937
973
  if (window.parent && window.parent !== window) {
@@ -954,46 +990,68 @@ Verificando autenticaci&oacute;n...
954
990
  function create3dsCallbackHandler(deps) {
955
991
  const logger = deps.logger ?? console;
956
992
  const { db } = deps.firebase;
957
- async function storeCres(orderId, cres) {
958
- if (orderId && cres) {
959
- try {
960
- await db.collection("orders").doc(orderId).update({
961
- threeDSCres: cres,
962
- updatedAt: /* @__PURE__ */ new Date()
963
- });
964
- } catch (err) {
965
- logger.error("Failed to store cres on order:", err);
993
+ async function persist(orderId, cres, transStatus) {
994
+ if (!orderId) return;
995
+ try {
996
+ const ref = db.collection("orders").doc(orderId);
997
+ const snap = await ref.get();
998
+ const status = snap.data()?.status;
999
+ if (!snap.exists || status !== "3ds-pending") {
1000
+ logger.warn(
1001
+ "[3ds-callback] rejected: order not in 3ds-pending state",
1002
+ { orderId, status }
1003
+ );
1004
+ return;
966
1005
  }
1006
+ const update = { updatedAt: /* @__PURE__ */ new Date() };
1007
+ if (cres) update.threeDSCres = cres;
1008
+ if (transStatus) update.threeDSTransStatus = transStatus;
1009
+ await ref.update(update);
1010
+ } catch (err) {
1011
+ logger.error("[3ds-callback] failed to store cres on order:", err);
967
1012
  }
968
1013
  }
1014
+ function resolveTransStatus(cres, fallback) {
1015
+ if (cres) {
1016
+ const fromCres = transStatusFromCres(cres, logger);
1017
+ if (fromCres) return fromCres;
1018
+ }
1019
+ return fallback;
1020
+ }
969
1021
  const POST = async function POST2(request) {
970
- const { searchParams } = new URL(request.url);
971
- const orderId = searchParams.get("orderId") || "";
1022
+ const orderId = new URL(request.url).searchParams.get("orderId") || "";
972
1023
  let transStatus = "U";
973
1024
  let cres = "";
974
1025
  try {
975
1026
  const contentType = request.headers.get("content-type") || "";
1027
+ const raw = await request.text();
976
1028
  if (contentType.includes("application/x-www-form-urlencoded")) {
977
- const text = await request.text();
978
- const params = new URLSearchParams(text);
979
- transStatus = params.get("transStatus") || "U";
980
- cres = params.get("cres") || params.get("CRes") || "";
1029
+ const obj = paramsToObject(new URLSearchParams(raw));
1030
+ transStatus = obj.transStatus || obj.TransStatus || "U";
1031
+ cres = pickCres(obj);
981
1032
  } else {
982
- const body = await request.json();
983
- transStatus = body.transStatus || "U";
984
- cres = body.cres || body.CRes || "";
1033
+ try {
1034
+ const body = JSON.parse(raw);
1035
+ transStatus = body.transStatus || body.TransStatus || "U";
1036
+ cres = pickCres(body);
1037
+ } catch {
1038
+ }
1039
+ }
1040
+ if (!cres && raw) {
1041
+ cres = pickCres(paramsToObject(new URLSearchParams(raw)));
985
1042
  }
986
1043
  } catch {
987
1044
  }
988
- await storeCres(orderId, cres);
1045
+ transStatus = resolveTransStatus(cres, transStatus);
1046
+ await persist(orderId, cres, transStatus);
989
1047
  return buildCompletionPage(orderId, transStatus);
990
1048
  };
991
1049
  const GET = async function GET2(request) {
992
- const { searchParams } = new URL(request.url);
993
- const orderId = searchParams.get("orderId") || "";
994
- const transStatus = searchParams.get("transStatus") || "Y";
995
- const cres = searchParams.get("cres") || searchParams.get("CRes") || "";
996
- await storeCres(orderId, cres);
1050
+ const params = new URL(request.url).searchParams;
1051
+ const orderId = params.get("orderId") || "";
1052
+ const cres = params.get("cres") || params.get("CRes") || params.get("value") || "";
1053
+ const transStatus = resolveTransStatus(cres, params.get("transStatus") || "Y");
1054
+ await persist(orderId, cres, transStatus);
997
1055
  return buildCompletionPage(orderId, transStatus);
998
1056
  };
999
1057
  return { POST, GET };