Skip to content

feat: PMM mode/allowedPaymentMethodTypes + NewCard wrapper#1807

Open
VeniaminC wants to merge 3 commits into
mainfrom
feat/pmm-mode-and-new-card
Open

feat: PMM mode/allowedPaymentMethodTypes + NewCard wrapper#1807
VeniaminC wants to merge 3 commits into
mainfrom
feat/pmm-mode-and-new-card

Conversation

@VeniaminC
Copy link
Copy Markdown
Contributor

Description

Updates CrossmintPaymentMethodManagement to match the new iframe contract from crossbit-main#24854, and adds CrossmintNewCard as a thin wrapper per the payments team's Option 2 (one underlying component, multiple named SDK entry points).

Props changes on CrossmintPaymentMethodManagement:

  • jwt is now optional. When omitted, the component tokenizes a card client-side without saving a UserPaymentMethod.
  • New mode: "select-or-add" | "add-only" — replaces the previous appearance.rules.SavedPaymentMethodsSection.display = "hidden" workaround.
  • New allowedPaymentMethodTypes: ("card")[] — future-proofs for bank-account etc.
  • appearance widened to the full EmbeddedCheckoutV3Appearance so consumers can pass rules.PrimaryButton for styling.
  • onPaymentMethodSelected now receives a discriminated union:
    • { type: "card", paymentMethod } — a saved method was selected/created
    • { type: "card-token", cardToken: { id, billing? } } — tokenize-only path (no JWT)

New component CrossmintNewCard:

<CrossmintNewCard onCardTokenized={(token) => ...} appearance={...} />

Internally just renders <CrossmintPaymentMethodManagement mode="add-only" allowedPaymentMethodTypes={["card"]} /> and narrows the callback to onCardTokenized.

Test plan

  • apps/payments/nextjs existing PaymentMethodManagement page still loads (backwards compatible, default mode is select-or-add)
  • Render CrossmintNewCard in isolation, tokenize a card, confirm onCardTokenized fires with { id, billing? }
  • Render CrossmintPaymentMethodManagement with mode="add-only" + JWT, confirm Continue creates a saved payment method and onPaymentMethodSelected receives { type: "card", paymentMethod }
  • Render CrossmintPaymentMethodManagement with appearance.rules.PrimaryButton.colors.background + borderRadius and verify styling applies
  • Typecheck clean: pnpm --filter @crossmint/client-sdk-base --filter @crossmint/client-sdk-react-ui tsc --noEmit

Package updates

  • @crossmint/client-sdk-base: minor (new props, discriminated union callback — breaking-ish but pre-1.0 style additive shape)
  • @crossmint/client-sdk-react-ui: minor (new CrossmintNewCard export)

Changeset: .changeset/pmm-mode-and-new-card.md

🤖 Generated with Claude Code

…d 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 <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 21, 2026

🦋 Changeset detected

Latest commit: d8e10ce

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@crossmint/client-sdk-base Minor
@crossmint/client-sdk-react-ui Minor
@crossmint/client-sdk-nextjs-starter Patch
@crossmint/client-sdk-auth Patch
@crossmint/client-sdk-react-base Patch
@crossmint/client-sdk-react-native-ui Patch
@crossmint/client-sdk-verifiable-credentials Patch
@crossmint/client-sdk-smart-wallet Patch
@crossmint/common-sdk-auth Patch
@crossmint/auth-ssr-nextjs-demo Patch
@crossmint/wallets-quickstart-devkit Patch
@crossmint/wallets-playground-react Patch
@crossmint/wallets-playground-expo Patch
@crossmint/server-sdk Patch
@crossmint/wallets-sdk Patch
crossmint-auth-node Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 21, 2026

Prompt To Fix All With AI
This 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.

Reviews (1): Last reviewed commit: "feat: PaymentMethodManagement mode/allow..." | Re-trigger Greptile

Comment on lines +15 to +23
const cardPaymentMethodSelectedSchema = z.object({
type: z.literal("card"),
paymentMethod: z
.object({
type: z.literal("card"),
paymentMethodId: z.string(),
})
.passthrough(),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Prompt To Fix With AI
This 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.

- 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 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 21, 2026

Prompt To Fix All With AI
This is a comment left during a code review.
Path: .changeset/pmm-mode-and-new-card.md
Line: 1-10

Comment:
**Removed public export not mentioned in changeset**

`PaymentMethodManagementAppearance` was a named export from `@crossmint/client-sdk-base` (via `CrossmintPaymentMethodManagementProps.ts``types/payment-method-management/index.ts`) and has been deleted entirely in this PR. The changeset describes the appearance prop as widened to `EmbeddedCheckoutV3Appearance`, but any consumer who imported `PaymentMethodManagementAppearance` as a type will get a compile error on upgrade. Consider adding a line to the changeset noting the removal, e.g. "The `PaymentMethodManagementAppearance` type has been removed — use `EmbeddedCheckoutV3Appearance` directly."

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "demo: add /new-card page and tighten inc..." | Re-trigger Greptile

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 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 21, 2026

Reviews (3): Last reviewed commit: "feat: CrossmintNewCard accepts optional ..." | Re-trigger Greptile

appearance?: PaymentMethodManagementAppearance;
jwt?: string;
mode?: PaymentMethodManagementMode;
allowedPaymentMethodTypes?: PaymentMethodManagementAllowedType[];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can u make this an object instead
i think its like:

allowedPaymentMethodTypes?: Partial<Record<PaymentMethodManagementAllowedType, boolean>>

export interface CrossmintPaymentMethodManagementProps {
jwt: string;
appearance?: PaymentMethodManagementAppearance;
jwt?: string;
Copy link
Copy Markdown
Collaborator

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

Comment on lines +37 to +46
export type CrossmintCardToken = {
id: string;
billing?: {
name?: string;
};
};

export type CrossmintPaymentMethod =
| { type: "card"; paymentMethod: CrossmintCardPaymentMethod }
| { type: "card-token"; cardToken: CrossmintCardToken };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since jwt required, remove card-token case

export interface CrossmintNewCardProps {
jwt?: string;
appearance?: EmbeddedCheckoutV3Appearance;
onCardTokenized?: (cardToken: CrossmintCardToken) => void | Promise<void>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, remove

import { CrossmintPaymentMethodManagement } from "./CrossmintPaymentMethodManagement";

export interface CrossmintNewCardProps {
jwt?: string;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make required

jwt?: string;
appearance?: EmbeddedCheckoutV3Appearance;
onCardTokenized?: (cardToken: CrossmintCardToken) => void | Promise<void>;
onPaymentMethodAdded?: (paymentMethod: CrossmintCardPaymentMethod) => void | Promise<void>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onCardAdded

Comment on lines +23 to +25
if (result.type === "card-token") {
return props.onCardTokenized?.(result.cardToken);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants