-
Notifications
You must be signed in to change notification settings - Fork 35
feat: PMM mode/allowedPaymentMethodTypes + NewCard wrapper #1807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| "use client"; | ||
|
|
||
| import { CrossmintProvider, CrossmintNewCard } from "@crossmint/client-sdk-react-ui"; | ||
|
|
||
| export default function NewCardPage() { | ||
| return ( | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| alignItems: "center", | ||
| justifyContent: "start", | ||
| padding: "20px", | ||
| }} | ||
| > | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| alignItems: "center", | ||
| justifyContent: "start", | ||
| width: "100%", | ||
| maxWidth: "450px", | ||
| }} | ||
| > | ||
| <CrossmintProvider apiKey="ck_development_5ZUNkuhjP8aYZEgUTDfWToqFpo5zakEqte1db4pHZgPAVKZ9JuTQKmeRbn1gv7zYCoZrRNYy4CnM7A3AMHQxFKA2BsSVeZbKEvXXY7126Th68mXhTg6oxHJpC2kuw9Q1HasVLX9LM67FoYSTRtTUUEzP93GUSEmeG5CZG7Lbop4oAQ7bmZUKTGmqN9L9wxP27CH13WaTBsrqxUJkojbKUXEd"> | ||
| <CrossmintNewCard | ||
| onCardTokenized={(cardToken) => { | ||
| console.log("card tokenized", cardToken); | ||
| }} | ||
| /> | ||
| </CrossmintProvider> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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[]; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can u make this an object instead
|
||
| appearance?: EmbeddedCheckoutV3Appearance; | ||
| onPaymentMethodSelected?: (paymentMethod: CrossmintPaymentMethod) => void | Promise<void>; | ||
| } | ||
|
|
||
| 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 }; | ||
|
Comment on lines
+37
to
+46
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since jwt required, remove card-token case |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(), | ||
| }), | ||
| }); | ||
|
Comment on lines
+15
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This causes a structural type mismatch: when the validated event data is passed to To fix, either extend the schema to include all required Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/client/base/src/types/payment-method-management/events/incoming.ts
Line: 15-23
Comment:
**Zod schema inferred type won't satisfy `CrossmintCardPaymentMethod`**
`cardPaymentMethodSelectedSchema` only declares `type` and `paymentMethodId` inside `paymentMethod`, relying on `.passthrough()` to preserve the remaining runtime fields. However, `.passthrough()` does not add the undeclared fields to Zod's inferred TypeScript output type — it only prevents them from being stripped at runtime. The inferred type for `paymentMethod` is therefore `{ type: "card"; paymentMethodId: string }`, which is missing the required `card: { source, brand, last4, expiration }` field of `CrossmintCardPaymentMethod`.
This causes a structural type mismatch: when the validated event data is passed to `props.onPaymentMethodSelected?.(data)` in `CrossmintPaymentMethodManagementIFrame.tsx`, TypeScript sees the Zod-inferred union rather than `CrossmintPaymentMethod`, and they are not mutually assignable. This will surface as a compile error on the unchanged IFrame component.
To fix, either extend the schema to include all required `CrossmintCardPaymentMethod` fields, or cast the validated data to `CrossmintPaymentMethod` at the call-site in the IFrame component.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import type { | ||
| CrossmintCardPaymentMethod, | ||
| CrossmintCardToken, | ||
| EmbeddedCheckoutV3Appearance, | ||
| } from "@crossmint/client-sdk-base"; | ||
| import { CrossmintPaymentMethodManagement } from "./CrossmintPaymentMethodManagement"; | ||
|
|
||
| export interface CrossmintNewCardProps { | ||
| jwt?: string; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make required |
||
| appearance?: EmbeddedCheckoutV3Appearance; | ||
| onCardTokenized?: (cardToken: CrossmintCardToken) => void | Promise<void>; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same, remove |
||
| onPaymentMethodAdded?: (paymentMethod: CrossmintCardPaymentMethod) => void | Promise<void>; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. onCardAdded |
||
| } | ||
|
|
||
| export function CrossmintNewCard(props: CrossmintNewCardProps) { | ||
| return ( | ||
| <CrossmintPaymentMethodManagement | ||
| jwt={props.jwt} | ||
| mode="add-only" | ||
| allowedPaymentMethodTypes={["card"]} | ||
| appearance={props.appearance} | ||
| onPaymentMethodSelected={(result) => { | ||
| if (result.type === "card-token") { | ||
| return props.onCardTokenized?.(result.cardToken); | ||
| } | ||
|
Comment on lines
+23
to
+25
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove |
||
| return props.onPaymentMethodAdded?.(result.paymentMethod); | ||
| }} | ||
| /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export * from "./CrossmintPaymentMethodManagement"; | ||
| export * from "./CrossmintNewCard"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for now lets just keep jwt as required, we will tackl non-authenticated cases later