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/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/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..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 @@ -1,9 +1,47 @@ 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(), + 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 = { "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..f140b64de --- /dev/null +++ b/packages/client/ui/react-ui/src/components/card-management/CrossmintNewCard.tsx @@ -0,0 +1,30 @@ +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 (result.type === "card-token") { + return props.onCardTokenized?.(result.cardToken); + } + return props.onPaymentMethodAdded?.(result.paymentMethod); + }} + /> + ); +} 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";