From 83952cb621b480897f79a877ad02f1a763bf57dc Mon Sep 17 00:00:00 2001 From: VeniaminC Date: Tue, 21 Apr 2026 12:38:32 -0400 Subject: [PATCH 1/3] feat: PaymentMethodManagement mode/allowedPaymentMethodTypes + NewCard wrapper Makes jwt optional, adds mode and allowedPaymentMethodTypes props, and introduces CrossmintNewCard as a tokenize-only wrapper. onPaymentMethodSelected now emits a discriminated union distinguishing saved-method selection from card tokenization. Co-Authored-By: Claude Opus 4.7 --- .changeset/pmm-mode-and-new-card.md | 10 ++++++++ .../CrossmintPaymentMethodManagementProps.ts | 23 +++++++++++++----- .../events/incoming.ts | 24 ++++++++++++++++++- .../card-management/CrossmintNewCard.tsx | 22 +++++++++++++++++ .../src/components/card-management/index.ts | 1 + 5 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 .changeset/pmm-mode-and-new-card.md create mode 100644 packages/client/ui/react-ui/src/components/card-management/CrossmintNewCard.tsx diff --git a/.changeset/pmm-mode-and-new-card.md b/.changeset/pmm-mode-and-new-card.md new file mode 100644 index 000000000..c16a249e7 --- /dev/null +++ b/.changeset/pmm-mode-and-new-card.md @@ -0,0 +1,10 @@ +--- +"@crossmint/client-sdk-base": minor +"@crossmint/client-sdk-react-ui": minor +--- + +`CrossmintPaymentMethodManagement`: add `mode` (`"select-or-add" | "add-only"`) and `allowedPaymentMethodTypes` props. Make `jwt` optional — when omitted, the component tokenizes the card without persisting a saved payment method and emits a `card-token` selection event. + +Introduces `CrossmintNewCard`, a tokenize-only wrapper around `CrossmintPaymentMethodManagement` for use cases that don't need a signed-in user (e.g., embedded checkout). + +The `onPaymentMethodSelected` callback now receives a discriminated union: `{ type: "card", paymentMethod }` when a saved method is selected/created, or `{ type: "card-token", cardToken }` when tokenizing without JWT. diff --git a/packages/client/base/src/types/payment-method-management/CrossmintPaymentMethodManagementProps.ts b/packages/client/base/src/types/payment-method-management/CrossmintPaymentMethodManagementProps.ts index a666a2b3c..444104041 100644 --- a/packages/client/base/src/types/payment-method-management/CrossmintPaymentMethodManagementProps.ts +++ b/packages/client/base/src/types/payment-method-management/CrossmintPaymentMethodManagementProps.ts @@ -1,12 +1,17 @@ import type { EmbeddedCheckoutV3Appearance } from "../embed"; +export type PaymentMethodManagementMode = "select-or-add" | "add-only"; +export type PaymentMethodManagementAllowedType = "card"; + export interface CrossmintPaymentMethodManagementProps { - jwt: string; - appearance?: PaymentMethodManagementAppearance; + jwt?: string; + mode?: PaymentMethodManagementMode; + allowedPaymentMethodTypes?: PaymentMethodManagementAllowedType[]; + appearance?: EmbeddedCheckoutV3Appearance; onPaymentMethodSelected?: (paymentMethod: CrossmintPaymentMethod) => void | Promise; } -export type CrossmintPaymentMethod = { +export type CrossmintCardPaymentMethod = { type: "card"; paymentMethodId: string; card: { @@ -29,7 +34,13 @@ export type CrossmintPaymentMethod = { }; }; -export type PaymentMethodManagementAppearance = { - fonts?: EmbeddedCheckoutV3Appearance["fonts"]; - variables?: EmbeddedCheckoutV3Appearance["variables"]; +export type CrossmintCardToken = { + id: string; + billing?: { + name?: string; + }; }; + +export type CrossmintPaymentMethod = + | { type: "card"; paymentMethod: CrossmintCardPaymentMethod } + | { type: "card-token"; cardToken: CrossmintCardToken }; diff --git a/packages/client/base/src/types/payment-method-management/events/incoming.ts b/packages/client/base/src/types/payment-method-management/events/incoming.ts index 8cdd5489f..ac9701a3f 100644 --- a/packages/client/base/src/types/payment-method-management/events/incoming.ts +++ b/packages/client/base/src/types/payment-method-management/events/incoming.ts @@ -1,9 +1,31 @@ import { z } from "zod"; +const cardTokenSelectedSchema = z.object({ + type: z.literal("card-token"), + cardToken: z.object({ + id: z.string(), + billing: z + .object({ + name: z.string().optional(), + }) + .optional(), + }), +}); + +const cardPaymentMethodSelectedSchema = z.object({ + type: z.literal("card"), + paymentMethod: z + .object({ + type: z.literal("card"), + paymentMethodId: z.string(), + }) + .passthrough(), +}); + export const paymentMethodManagementIncomingEvents = { "ui:height.changed": z.object({ height: z.number(), }), - "payment-method:selected": z.any(), + "payment-method:selected": z.discriminatedUnion("type", [cardPaymentMethodSelectedSchema, cardTokenSelectedSchema]), }; export type PaymentMethodManagementIncomingEventMap = typeof paymentMethodManagementIncomingEvents; diff --git a/packages/client/ui/react-ui/src/components/card-management/CrossmintNewCard.tsx b/packages/client/ui/react-ui/src/components/card-management/CrossmintNewCard.tsx new file mode 100644 index 000000000..3918535af --- /dev/null +++ b/packages/client/ui/react-ui/src/components/card-management/CrossmintNewCard.tsx @@ -0,0 +1,22 @@ +import type { CrossmintCardToken, EmbeddedCheckoutV3Appearance } from "@crossmint/client-sdk-base"; +import { CrossmintPaymentMethodManagement } from "./CrossmintPaymentMethodManagement"; + +export interface CrossmintNewCardProps { + appearance?: EmbeddedCheckoutV3Appearance; + onCardTokenized?: (cardToken: CrossmintCardToken) => void | Promise; +} + +export function CrossmintNewCard(props: CrossmintNewCardProps) { + return ( + { + if (paymentMethod.type === "card-token") { + return props.onCardTokenized?.(paymentMethod.cardToken); + } + }} + /> + ); +} diff --git a/packages/client/ui/react-ui/src/components/card-management/index.ts b/packages/client/ui/react-ui/src/components/card-management/index.ts index ea4d02023..f1f46bba9 100644 --- a/packages/client/ui/react-ui/src/components/card-management/index.ts +++ b/packages/client/ui/react-ui/src/components/card-management/index.ts @@ -1 +1,2 @@ export * from "./CrossmintPaymentMethodManagement"; +export * from "./CrossmintNewCard"; From a7a2f027e0e3cdc70ec0730de07447038ad42dca Mon Sep 17 00:00:00 2001 From: VeniaminC Date: Tue, 21 Apr 2026 13:07:14 -0400 Subject: [PATCH 2/3] demo: add /new-card page and tighten incoming event schema - Adds apps/payments/nextjs/app/new-card exercising CrossmintNewCard - Tightens payment-method:selected schema to validate the full card shape, so the TS callback type matches CrossmintPaymentMethod Co-Authored-By: Claude Opus 4.7 --- apps/payments/nextjs/app/new-card/page.tsx | 36 +++++++++++++++++++ .../events/incoming.ts | 28 +++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 apps/payments/nextjs/app/new-card/page.tsx diff --git a/apps/payments/nextjs/app/new-card/page.tsx b/apps/payments/nextjs/app/new-card/page.tsx new file mode 100644 index 000000000..82802b67a --- /dev/null +++ b/apps/payments/nextjs/app/new-card/page.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { CrossmintProvider, CrossmintNewCard } from "@crossmint/client-sdk-react-ui"; + +export default function NewCardPage() { + return ( +
+
+ + { + console.log("card tokenized", cardToken); + }} + /> + +
+
+ ); +} diff --git a/packages/client/base/src/types/payment-method-management/events/incoming.ts b/packages/client/base/src/types/payment-method-management/events/incoming.ts index ac9701a3f..67ff68da7 100644 --- a/packages/client/base/src/types/payment-method-management/events/incoming.ts +++ b/packages/client/base/src/types/payment-method-management/events/incoming.ts @@ -14,12 +14,28 @@ const cardTokenSelectedSchema = z.object({ const cardPaymentMethodSelectedSchema = z.object({ type: z.literal("card"), - paymentMethod: z - .object({ - type: z.literal("card"), - paymentMethodId: z.string(), - }) - .passthrough(), + paymentMethod: z.object({ + type: z.literal("card"), + paymentMethodId: z.string(), + card: z.object({ + source: z.object({ + type: z.literal("basis-theory-token"), + id: z.string(), + }), + brand: z.string(), + last4: z.string(), + expiration: z.object({ + month: z.string(), + year: z.string(), + }), + }), + default: z.boolean().optional(), + display: z + .object({ + imageUrl: z.string().optional(), + }) + .optional(), + }), }); export const paymentMethodManagementIncomingEvents = { From d8e10ce473039c6bb30a25263ff40a8fde9b6c3d Mon Sep 17 00:00:00 2001 From: VeniaminC Date: Tue, 21 Apr 2026 13:13:02 -0400 Subject: [PATCH 3/3] feat: CrossmintNewCard accepts optional jwt with split callbacks When jwt is provided the card is both tokenized and saved as a UserPaymentMethod (onPaymentMethodAdded fires); without a jwt only the BT token is emitted (onCardTokenized fires). Either or both callbacks can be wired independently. Co-Authored-By: Claude Opus 4.7 --- .../card-management/CrossmintNewCard.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/client/ui/react-ui/src/components/card-management/CrossmintNewCard.tsx b/packages/client/ui/react-ui/src/components/card-management/CrossmintNewCard.tsx index 3918535af..f140b64de 100644 --- a/packages/client/ui/react-ui/src/components/card-management/CrossmintNewCard.tsx +++ b/packages/client/ui/react-ui/src/components/card-management/CrossmintNewCard.tsx @@ -1,21 +1,29 @@ -import type { CrossmintCardToken, EmbeddedCheckoutV3Appearance } from "@crossmint/client-sdk-base"; +import type { + CrossmintCardPaymentMethod, + CrossmintCardToken, + EmbeddedCheckoutV3Appearance, +} from "@crossmint/client-sdk-base"; import { CrossmintPaymentMethodManagement } from "./CrossmintPaymentMethodManagement"; export interface CrossmintNewCardProps { + jwt?: string; appearance?: EmbeddedCheckoutV3Appearance; onCardTokenized?: (cardToken: CrossmintCardToken) => void | Promise; + onPaymentMethodAdded?: (paymentMethod: CrossmintCardPaymentMethod) => void | Promise; } export function CrossmintNewCard(props: CrossmintNewCardProps) { return ( { - if (paymentMethod.type === "card-token") { - return props.onCardTokenized?.(paymentMethod.cardToken); + onPaymentMethodSelected={(result) => { + if (result.type === "card-token") { + return props.onCardTokenized?.(result.cardToken); } + return props.onPaymentMethodAdded?.(result.paymentMethod); }} /> );