adf6eb572b
## What & why Replaces the hosted Stripe Checkout redirect on the onboarding "Choose your plan" step (credit-card trial) with an inline Stripe **Payment Element**, so users never leave the app to enter card details. ## How it works - **Frontend:** a deferred `<Elements mode="setup">` renders the Payment Element, themed via the Appearance API. On Continue: `elements.submit()` → `checkoutSession` mutation creates the trialing subscription server-side and returns its pending SetupIntent `clientSecret` → `stripe.confirmSetup()` confirms the card (handling 3DS) → redirect to the existing `/plan-required/payment-success`. - **Backend:** new `BILLING_STRIPE_PUBLISHABLE_KEY` config var exposed via `/client-config`; the card path creates the subscription with `payment_behavior: default_incomplete` + a free trial (so Stripe attaches a `pending_setup_intent`) and returns its client secret. The hosted-Checkout code path is removed. - The **no-credit-card** trial path is unchanged. - Billing address collection is **disabled** in the Payment Element to reduce friction; `automatic_tax` is correspondingly disabled (tax needs an address — collect it later, e.g. at conversion / via the billing portal). ## Required before this works 1. Set `BILLING_STRIPE_PUBLISHABLE_KEY` (`pk_…`) on the server (infra change pending). 2. Run `nx run twenty-front:graphql:generate --configuration=metadata` against a server exposing the updated schema (see inline note on the hand-authored document). 3. Verify in Stripe test mode: happy path, 3DS (`4000 0025 0000 3155`), a decline. ## Verified typecheck (front + server), oxlint + oxfmt clean, `client-config.service.spec` passing. Not run here: the app end-to-end / Stripe test mode and `graphql:generate` (no server/DB in the dev container). I've left self-review comments inline flagging cleanup opportunities plus a couple of architectural/tech-debt items. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TxCfinXq7abSrbF7aTw2cA --- _Generated by [Claude Code](https://claude.ai/code/session_01TxCfinXq7abSrbF7aTw2cA)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21759?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude <noreply@anthropic.com>
39 lines
1.1 KiB
TypeScript
39 lines
1.1 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
type TrialCardProps = {
|
|
duration: number;
|
|
withCreditCard: boolean;
|
|
};
|
|
|
|
const StyledTrialCardContainer = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
`;
|
|
|
|
const StyledTrialDurationContainer = styled.div`
|
|
color: ${themeCssVariables.font.color.secondary};
|
|
display: flex;
|
|
font-size: ${themeCssVariables.font.size.md};
|
|
margin-bottom: ${themeCssVariables.spacing[2]};
|
|
`;
|
|
|
|
const StyledCreditCardRequirementContainer = styled.div`
|
|
color: ${themeCssVariables.font.color.tertiary};
|
|
display: flex;
|
|
font-size: ${themeCssVariables.font.size.md};
|
|
`;
|
|
|
|
export const TrialCard = ({ duration, withCreditCard }: TrialCardProps) => {
|
|
const { t } = useLingui();
|
|
return (
|
|
<StyledTrialCardContainer>
|
|
<StyledTrialDurationContainer>{t`${duration} days trial`}</StyledTrialDurationContainer>
|
|
<StyledCreditCardRequirementContainer>
|
|
{withCreditCard ? t`With Credit Card` : t`No Credit Card`}
|
|
</StyledCreditCardRequirementContainer>
|
|
</StyledTrialCardContainer>
|
|
);
|
|
};
|