Skip to content

Commit

Permalink
feat: allow managing addons from the checkout flow (#8407)
Browse files Browse the repository at this point in the history
* update types

* setup new page

* reconstruct cart from existing subscription

* fix typecheck

* feat: confirmation step

* integrate endpoint to update subscription

* change wording

* feat: adjust logic and update summary

* fix typecheck
  • Loading branch information
alexnm committed Mar 29, 2024
1 parent 4df5a84 commit 0b4e0a8
Show file tree
Hide file tree
Showing 23 changed files with 876 additions and 218 deletions.
6 changes: 4 additions & 2 deletions packages/app/src/app/components/WorkspaceSetup/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import React from 'react';
import { Stack, ThemeProvider } from '@codesandbox/components';
import styled, { keyframes } from 'styled-components';
import { Summary } from './Summary';
import { WorkspaceFlow } from './types';

export const WorkspaceFlowLayout: React.FC<{
flow: WorkspaceFlow;
showSummary: boolean;
allowSummaryChanges: boolean;
}> = ({ children, showSummary, allowSummaryChanges }) => {
}> = ({ children, showSummary, allowSummaryChanges, flow }) => {
return (
<ThemeProvider>
<Stack
Expand Down Expand Up @@ -49,7 +51,7 @@ export const WorkspaceFlowLayout: React.FC<{
</Stack>
{showSummary && (
<SlidePanel>
<Summary allowChanges={allowSummaryChanges} />
<Summary flow={flow} allowChanges={allowSummaryChanges} />
</SlidePanel>
)}
</Stack>
Expand Down
234 changes: 137 additions & 97 deletions packages/app/src/app/components/WorkspaceSetup/Summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,33 @@ import { IconButton, Stack, Text, Switch } from '@codesandbox/components';
import { useActions, useAppState } from 'app/overmind';
import styled from 'styled-components';
import { useWorkspaceSubscription } from 'app/hooks/useWorkspaceSubscription';
import { useLocation } from 'react-router-dom';
import {
CreditAddon,
SubscriptionPackage,
} from 'app/overmind/namespaces/checkout/types';
import { fadeAnimation } from './elements';
import { WorkspaceFlow } from './types';

export const Summary: React.FC<{ allowChanges: boolean }> = ({
allowChanges,
}) => {
export const Summary: React.FC<{
allowChanges: boolean;
flow: WorkspaceFlow;
}> = ({ allowChanges, flow }) => {
const actions = useActions();
const { isPro } = useWorkspaceSubscription();
const { pathname } = useLocation();
const isUpgrading = pathname.includes('upgrade');
const { checkout } = useAppState();
const {
selectedPlan,
creditAddons,
totalCredits,
totalPrice,
currentSubscription,
newSubscription,
spendingLimit,
availableBasePlans,
hasUpcomingChange,
} = checkout;

const basePlan = availableBasePlans[selectedPlan];
const isAnnual = selectedPlan === 'flex-annual';

if (!basePlan) {
return null;
}
const isAnnual = newSubscription?.basePlan.id === 'flex-annual';
const allowAnnualSwitch = flow !== 'manage-addons';

return (
<Stack
gap={10}
gap={16}
direction="vertical"
css={{
padding: '64px 48px',
Expand All @@ -42,91 +39,59 @@ export const Summary: React.FC<{ allowChanges: boolean }> = ({
},
}}
>
<Text size={6} color="#fff">
Plan summary
</Text>

<Stack
direction="vertical"
gap={6}
css={{ paddingBottom: '24px', borderBottom: '1px solid #5C5C5C' }}
>
<Stack direction="horizontal" justify="space-between" gap={2}>
<Stack direction="vertical">
<Text color="#fff">{basePlan.name} plan base</Text>
<Text>{basePlan.credits} VM credits</Text>
</Stack>
<Text color="#fff">${basePlan.price}</Text>
</Stack>

{creditAddons.map(item => (
<AnimatedLineItem
direction="horizontal"
key={item.addon.id}
align="center"
justify="space-between"
gap={2}
>
<Text color="#fff">{item.addon.credits} VM credits</Text>
<Stack align="center">
{allowChanges && (
<QuantityCounter
quantity={item.quantity}
onIncrement={() => {
actions.checkout.addCreditsPackage(item.addon);
track('Checkout - Increment Addon Item', {
from: isUpgrading ? 'upgrade' : 'create-workspace',
currentPlan: isPro ? 'pro' : 'free',
});
}}
onDecrement={() => {
actions.checkout.removeCreditsPackage(item.addon.id);
track('Checkout - Decrement Addon Item', {
from: isUpgrading ? 'upgrade' : 'create-workspace',
currentPlan: isPro ? 'pro' : 'free',
});
}}
/>
)}

<Text color="#fff" css={{ width: '48px', textAlign: 'right' }}>
${item.quantity * item.addon.price}
</Text>
</Stack>
</AnimatedLineItem>
))}
</Stack>

<Stack justify="space-between">
<Stack direction="vertical">
<Text color="#fff">Total cost per {isAnnual ? 'year' : 'month'}</Text>
<Text>{totalCredits} VM credits</Text>
</Stack>

<Text color="#fff">${totalPrice}</Text>
</Stack>

<Stack css={{ gap: '8px' }}>
<Switch
id="recurring"
on={isAnnual}
onChange={() => {
actions.checkout.selectPlan(isAnnual ? 'flex' : 'flex-annual');
{currentSubscription && hasUpcomingChange && (
<PlanSummary
title="Current plan"
subscriptionPackage={currentSubscription}
editable={false}
/>
)}

track('Checkout - Toggle recurring type', {
from: 'summary',
newValue: isAnnual ? 'annual' : 'monthly',
{newSubscription && (
<PlanSummary
title={currentSubscription ? 'New plan' : 'Plan summary'}
subscriptionPackage={newSubscription}
editable={allowChanges}
onIncrementItem={addon => {
actions.checkout.addCreditsPackage(addon);
track('Checkout - Increment Addon Item', {
from: flow,
currentPlan: isPro ? 'pro' : 'free',
});
}}
onDecrementItem={addon => {
actions.checkout.removeCreditsPackage(addon);
track('Checkout - Decrement Addon Item', {
from: flow,
currentPlan: isPro ? 'pro' : 'free',
});
}}
/>
<Stack direction="vertical" css={{ marginTop: -3 }}>
<Text color="#fff" as="label" htmlFor="recurring">
Annual (Save 30%)
</Text>
)}

{allowAnnualSwitch && (
<Stack css={{ gap: '8px' }}>
<Switch
id="recurring"
on={isAnnual}
onChange={() => {
actions.checkout.selectPlan(isAnnual ? 'flex' : 'flex-annual');

{isAnnual && <Text>24 hour processing time</Text>}
track('Checkout - Toggle recurring type', {
from: flow,
newValue: isAnnual ? 'annual' : 'monthly',
});
}}
/>
<Stack direction="vertical" css={{ marginTop: -3 }}>
<Text color="#fff" as="label" htmlFor="recurring">
Annual (Save 30%)
</Text>

{isAnnual && <Text>24 hour processing time</Text>}
</Stack>
</Stack>
</Stack>
)}

<Text size={3}>
Additional VM credits are available on-demand for $0.018/credit.
Expand All @@ -137,6 +102,81 @@ export const Summary: React.FC<{ allowChanges: boolean }> = ({
);
};

interface PlanSummaryProps {
title: string;
subscriptionPackage: SubscriptionPackage;
editable: boolean;
onDecrementItem?: (addon: CreditAddon) => void;
onIncrementItem?: (addon: CreditAddon) => void;
}

const PlanSummary: React.FC<PlanSummaryProps> = ({
title,
subscriptionPackage,
editable,
onDecrementItem,
onIncrementItem,
}) => (
<Stack direction="vertical" gap={6}>
<Text size={6} color="#fff">
{title}
</Text>

<Stack
direction="vertical"
gap={4}
css={{ paddingBottom: '24px', borderBottom: '1px solid #5C5C5C' }}
>
<Stack direction="horizontal" justify="space-between" gap={2}>
<Stack direction="vertical">
<Text color="#fff">
{subscriptionPackage.basePlan.name} plan base
</Text>
<Text>{subscriptionPackage.basePlan.credits} VM credits</Text>
</Stack>
<Text color="#fff">${subscriptionPackage.basePlan.price}</Text>
</Stack>

{subscriptionPackage.addonItems.map(item => (
<AnimatedLineItem
direction="horizontal"
key={item.addon.id}
align="center"
justify="space-between"
gap={2}
>
<Text color="#fff">{item.addon.credits} VM credits</Text>
<Stack align="center">
{editable && (
<QuantityCounter
quantity={item.quantity}
onIncrement={() => onIncrementItem?.(item.addon)}
onDecrement={() => onDecrementItem?.(item.addon)}
/>
)}

<Text color="#fff" css={{ width: '48px', textAlign: 'right' }}>
${item.quantity * item.addon.price}
</Text>
</Stack>
</AnimatedLineItem>
))}
</Stack>

<Stack justify="space-between">
<Stack direction="vertical">
<Text color="#fff">
Total cost per{' '}
{subscriptionPackage.basePlan.id === 'flex-annual' ? 'year' : 'month'}
</Text>
<Text>{subscriptionPackage.totalCredits} VM credits</Text>
</Stack>

<Text color="#fff">${subscriptionPackage.totalPrice}</Text>
</Stack>
</Stack>
);

const AnimatedLineItem = styled(Stack)`
animation: ${fadeAnimation};
height: 28px;
Expand Down
14 changes: 12 additions & 2 deletions packages/app/src/app/components/WorkspaceSetup/WorkspaceSetup.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React from 'react';
import { WorkspaceFlowLayout } from './Layout';
import { StepProps, WorkspaceSetupStep } from './types';
import { StepProps, WorkspaceSetupStep, WorkspaceFlow } from './types';
import { Create } from './steps/Create';
import { Plans } from './steps/Plans';
import { SpendingLimit } from './steps/SpendingLimit';
import { SelectWorkspace } from './steps/SelectWorkspace';
import { Addons } from './steps/Addons';
import { Finalize } from './steps/Finalize';
import { ChangeAddons } from './steps/ChangeAddons';

export type WorkspaceSetupProps = {
flow: WorkspaceFlow;
steps: WorkspaceSetupStep[];
startFrom?: WorkspaceSetupStep; // when this isn't passed, first one from the array is used
onComplete: (fullReload?: boolean) => void;
onDismiss: () => void;
};

export const WorkspaceSetup: React.FC<WorkspaceSetupProps> = ({
flow,
steps,
startFrom,
onComplete,
Expand All @@ -38,8 +41,10 @@ export const WorkspaceSetup: React.FC<WorkspaceSetupProps> = ({
<WorkspaceFlowLayout
showSummary={STEPS_WITH_CHECKOUT.includes(currentStep)}
allowSummaryChanges={currentStep === 'addons'}
flow={flow}
>
<Component
flow={flow}
currentStep={currentStepIndex}
numberOfSteps={steps.length}
onPrevStep={() => setCurrentStepIndex(crtStepIndex => crtStepIndex - 1)}
Expand All @@ -58,6 +63,11 @@ const STEP_COMPONENTS: Record<WorkspaceSetupStep, React.FC<StepProps>> = {
'spending-limit': SpendingLimit,
addons: Addons,
finalize: Finalize,
'change-addons-confirmation': ChangeAddons,
};

const STEPS_WITH_CHECKOUT: WorkspaceSetupStep[] = ['spending-limit', 'addons'];
const STEPS_WITH_CHECKOUT: WorkspaceSetupStep[] = [
'spending-limit',
'addons',
'change-addons-confirmation',
];
Loading

0 comments on commit 0b4e0a8

Please sign in to comment.