mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-27 05:33:10 +03:00
feat(core): impl subscription plans setting
This commit is contained in:
parent
df054ac7f6
commit
1d62133f4f
@ -9,6 +9,7 @@ import type { ReactElement, SVGProps } from 'react';
|
||||
|
||||
import { AboutAffine } from './about';
|
||||
import { AppearanceSettings } from './appearance';
|
||||
import { AFFiNECloudPlans } from './plans';
|
||||
import { Plugins } from './plugins';
|
||||
import { Shortcuts } from './shortcuts';
|
||||
|
||||
@ -16,7 +17,9 @@ export type GeneralSettingKeys =
|
||||
| 'shortcuts'
|
||||
| 'appearance'
|
||||
| 'plugins'
|
||||
| 'about';
|
||||
| 'about'
|
||||
| 'plans'
|
||||
| 'billing';
|
||||
|
||||
interface GeneralSettingListItem {
|
||||
key: GeneralSettingKeys;
|
||||
@ -43,6 +46,22 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
icon: KeyboardIcon,
|
||||
testId: 'shortcuts-panel-trigger',
|
||||
},
|
||||
{
|
||||
key: 'plans',
|
||||
// TODO: i18n
|
||||
title: 'AFFiNE Cloud Plans',
|
||||
// TODO: icon
|
||||
icon: KeyboardIcon,
|
||||
testId: 'plans-panel-trigger',
|
||||
},
|
||||
{
|
||||
key: 'billing',
|
||||
// TODO: i18n
|
||||
title: 'Billing',
|
||||
// TODO: icon
|
||||
icon: KeyboardIcon,
|
||||
testId: 'billing-panel-trigger',
|
||||
},
|
||||
{
|
||||
key: 'plugins',
|
||||
title: 'Plugins',
|
||||
@ -72,6 +91,8 @@ export const GeneralSetting = ({ generalKey }: GeneralSettingProps) => {
|
||||
return <Plugins />;
|
||||
case 'about':
|
||||
return <AboutAffine />;
|
||||
case 'plans':
|
||||
return <AFFiNECloudPlans />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -0,0 +1,417 @@
|
||||
import { RadioButton, RadioButtonGroup } from '@affine/component';
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import {
|
||||
cancelSubscriptionMutation,
|
||||
checkoutMutation,
|
||||
pricesQuery,
|
||||
SubscriptionPlan,
|
||||
subscriptionQuery,
|
||||
SubscriptionRecurring,
|
||||
updateSubscriptionMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
|
||||
interface FixedPrice {
|
||||
type: 'fixed';
|
||||
plan: SubscriptionPlan;
|
||||
price: string;
|
||||
yearlyPrice: string;
|
||||
discount?: string;
|
||||
benefits: string[];
|
||||
}
|
||||
|
||||
interface DynamicPrice {
|
||||
type: 'dynamic';
|
||||
plan: SubscriptionPlan;
|
||||
contact: boolean;
|
||||
benefits: string[];
|
||||
}
|
||||
|
||||
// TODO: i18n all things
|
||||
const planDetail = new Map<SubscriptionPlan, FixedPrice | DynamicPrice>([
|
||||
[
|
||||
SubscriptionPlan.Free,
|
||||
{
|
||||
type: 'fixed',
|
||||
plan: SubscriptionPlan.Free,
|
||||
price: '0',
|
||||
yearlyPrice: '0',
|
||||
benefits: [
|
||||
'Unlimited local workspace',
|
||||
'Unlimited login devices',
|
||||
'Unlimited blocks',
|
||||
'AFFiNE Cloud Storage 10GB',
|
||||
'The maximum file size is 10M',
|
||||
'Number of members per Workspace ≤ 3',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
SubscriptionPlan.Pro,
|
||||
{
|
||||
type: 'fixed',
|
||||
plan: SubscriptionPlan.Pro,
|
||||
price: '1',
|
||||
yearlyPrice: '1',
|
||||
benefits: [
|
||||
'Unlimited local workspace',
|
||||
'Unlimited login devices',
|
||||
'Unlimited blocks',
|
||||
'AFFiNE Cloud Storage 100GB',
|
||||
'The maximum file size is 500M',
|
||||
'Number of members per Workspace ≤ 10',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
SubscriptionPlan.Team,
|
||||
{
|
||||
type: 'dynamic',
|
||||
plan: SubscriptionPlan.Team,
|
||||
contact: true,
|
||||
benefits: [
|
||||
'Best team workspace for collaboration and knowledge distilling.',
|
||||
'Focusing on what really matters with team project management and automation.',
|
||||
'Pay for seats, fits all team size.',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
SubscriptionPlan.Enterprise,
|
||||
{
|
||||
type: 'dynamic',
|
||||
plan: SubscriptionPlan.Enterprise,
|
||||
contact: true,
|
||||
benefits: [
|
||||
'Solutions & best practices for dedicated needs.',
|
||||
'Embedable & interrogations with IT support.',
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const Settings = () => {
|
||||
const { data, mutate } = useQuery({
|
||||
query: subscriptionQuery,
|
||||
});
|
||||
|
||||
const {
|
||||
data: { prices },
|
||||
} = useQuery({
|
||||
query: pricesQuery,
|
||||
});
|
||||
|
||||
prices.forEach(price => {
|
||||
const detail = planDetail.get(price.plan);
|
||||
|
||||
if (detail?.type === 'fixed') {
|
||||
detail.price = (price.amount / 100).toFixed(2);
|
||||
detail.yearlyPrice = (price.yearlyAmount / 100 / 12).toFixed(2);
|
||||
detail.discount = (
|
||||
(1 - price.yearlyAmount / 12 / price.amount) *
|
||||
100
|
||||
).toFixed(2);
|
||||
}
|
||||
});
|
||||
|
||||
const loggedIn = !!data.currentUser;
|
||||
const subscription = data.currentUser?.subscription;
|
||||
|
||||
const [recurring, setRecurring] = useState<string>(
|
||||
subscription?.recurring ?? SubscriptionRecurring.Monthly
|
||||
);
|
||||
|
||||
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
const currentRecurring =
|
||||
subscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
mutate();
|
||||
}, [mutate]);
|
||||
|
||||
const yearlyDiscount = (
|
||||
planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined
|
||||
)?.discount;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title="Plans"
|
||||
subtitle={
|
||||
// TODO: different subtitle for un-logged user
|
||||
<p>
|
||||
You are current on the {currentPlan} plan. If you have any
|
||||
questions, please contact our{' '}
|
||||
<span>{/*TODO: add action*/}customer support</span>.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<div className={styles.wrapper}>
|
||||
<RadioButtonGroup
|
||||
className={styles.recurringRadioGroup}
|
||||
value={recurring}
|
||||
onValueChange={setRecurring}
|
||||
>
|
||||
{Object.values(SubscriptionRecurring).map(plan => (
|
||||
<RadioButton key={plan} value={plan}>
|
||||
{plan}
|
||||
{plan === SubscriptionRecurring.Yearly && yearlyDiscount && (
|
||||
<span className={styles.radioButtonDiscount}>
|
||||
{yearlyDiscount}% off
|
||||
</span>
|
||||
)}
|
||||
</RadioButton>
|
||||
))}
|
||||
</RadioButtonGroup>
|
||||
{/* TODO: plan cards horizontal scroll behavior is not the same as design */}
|
||||
{/* TODO: may scroll current plan into view when first loading? */}
|
||||
<div className={styles.planCardsWrapper}>
|
||||
{Array.from(planDetail.values()).map(detail => {
|
||||
const isCurrent =
|
||||
currentPlan === detail.plan && currentRecurring === recurring;
|
||||
return (
|
||||
<div
|
||||
key={detail.plan}
|
||||
className={
|
||||
loggedIn && currentPlan === detail.plan
|
||||
? styles.currentPlanCard
|
||||
: styles.planCard
|
||||
}
|
||||
>
|
||||
<div className={styles.planTitle}>
|
||||
<p>
|
||||
{detail.plan}{' '}
|
||||
{'discount' in detail && (
|
||||
<span className={styles.discountLabel}>
|
||||
{detail.discount}% off
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<span className={styles.planPrice}>
|
||||
$
|
||||
{detail.type === 'dynamic'
|
||||
? '?'
|
||||
: recurring === SubscriptionRecurring.Monthly
|
||||
? detail.price
|
||||
: detail.yearlyPrice}
|
||||
</span>
|
||||
<span className={styles.planPriceDesc}>per month</span>
|
||||
</p>
|
||||
{
|
||||
// branches:
|
||||
// if contact => 'Contact Sales'
|
||||
// if not signed in:
|
||||
// if free => 'Sign up free'
|
||||
// else => 'Buy Pro'
|
||||
// else
|
||||
// if isCurrent => 'Current Plan'
|
||||
// else if free => 'Downgrade'
|
||||
// else if currentRecurring !== recurring => 'Change to {recurring} Billing'
|
||||
// else => 'Upgrade'
|
||||
// TODO: should replace with components with proper actions
|
||||
detail.type === 'dynamic' ? (
|
||||
<ContactSales />
|
||||
) : loggedIn ? (
|
||||
isCurrent ? (
|
||||
<CurrentPlan />
|
||||
) : detail.plan === SubscriptionPlan.Free ? (
|
||||
<Downgrade onActionDone={refresh} />
|
||||
) : currentRecurring !== recurring ? (
|
||||
<ChangeRecurring
|
||||
from={currentRecurring}
|
||||
to={recurring as SubscriptionRecurring}
|
||||
onActionDone={refresh}
|
||||
/>
|
||||
) : (
|
||||
<Upgrade recurring={recurring} onActionDone={refresh} />
|
||||
)
|
||||
) : (
|
||||
<SignupAction>
|
||||
{detail.plan === SubscriptionPlan.Free
|
||||
? 'Sign up free'
|
||||
: 'Buy Pro'}
|
||||
</SignupAction>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className={styles.planBenefits}>
|
||||
{detail.benefits.map((content, i) => (
|
||||
<div key={i} className={styles.planBenefit}>
|
||||
<div className={styles.planBenefitIcon}>
|
||||
{/* TODO: icons */}
|
||||
{detail.type == 'dynamic' ? '·' : '✅'}
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<a
|
||||
className={styles.allPlansLink}
|
||||
href="https://affine.pro/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
See all plans →{/* TODO: icon */}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Downgrade = ({ onActionDone }: { onActionDone: () => void }) => {
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: cancelSubscriptionMutation,
|
||||
});
|
||||
|
||||
const downgrade = useCallback(() => {
|
||||
trigger(null, { onSuccess: onActionDone });
|
||||
}, [trigger, onActionDone]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
type="primary"
|
||||
onClick={downgrade /* TODO: poppup confirmation modal instead */}
|
||||
disabled={isMutating}
|
||||
loading={isMutating}
|
||||
>
|
||||
Downgrade
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const Upgrade = ({
|
||||
recurring,
|
||||
onActionDone,
|
||||
}: {
|
||||
recurring: SubscriptionRecurring;
|
||||
onActionDone: () => void;
|
||||
}) => {
|
||||
const { isMutating, trigger, data } = useMutation({
|
||||
mutation: checkoutMutation,
|
||||
});
|
||||
|
||||
const upgrade = useCallback(() => {
|
||||
trigger({ recurring });
|
||||
}, [trigger, recurring]);
|
||||
|
||||
const newTabRef = useRef<Window | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.checkout) {
|
||||
if (newTabRef.current) {
|
||||
newTabRef.current.focus();
|
||||
} else {
|
||||
// FIXME: safari prevents from opening new tab by window api
|
||||
// TODO(@xp): what if electron?
|
||||
const newTab = window.open(
|
||||
data.checkout,
|
||||
'_blank',
|
||||
'noopener noreferrer'
|
||||
);
|
||||
|
||||
if (newTab) {
|
||||
newTabRef.current = newTab;
|
||||
const update = () => {
|
||||
onActionDone();
|
||||
};
|
||||
newTab.addEventListener('close', update);
|
||||
|
||||
return () => newTab.removeEventListener('close', update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}, [data?.checkout, onActionDone]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
type="primary"
|
||||
onClick={upgrade}
|
||||
disabled={isMutating}
|
||||
loading={isMutating}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const ChangeRecurring = ({
|
||||
from: _from /* TODO: from can be useful when showing confirmation modal */,
|
||||
to,
|
||||
onActionDone,
|
||||
}: {
|
||||
from: SubscriptionRecurring;
|
||||
to: SubscriptionRecurring;
|
||||
onActionDone: () => void;
|
||||
}) => {
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: updateSubscriptionMutation,
|
||||
});
|
||||
|
||||
const change = useCallback(() => {
|
||||
trigger({ recurring: to }, { onSuccess: onActionDone });
|
||||
}, [trigger, onActionDone, to]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
type="primary"
|
||||
onClick={change /* TODO: popup confirmation modal instead */}
|
||||
disabled={isMutating}
|
||||
loading={isMutating}
|
||||
>
|
||||
Change to {to} Billing
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const ContactSales = () => {
|
||||
return (
|
||||
// TODO: add action
|
||||
<Button className={styles.planAction} type="primary">
|
||||
Contact Sales
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const CurrentPlan = () => {
|
||||
return <Button className={styles.planAction}>Current Plan</Button>;
|
||||
};
|
||||
|
||||
const SignupAction = ({ children }: PropsWithChildren) => {
|
||||
// TODO: add login action
|
||||
return (
|
||||
<Button className={styles.planAction} type="primary">
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const AFFiNECloudPlans = () => {
|
||||
return (
|
||||
// TODO: loading skeleton
|
||||
// TODO: Error Boundary
|
||||
<Suspense>
|
||||
<Settings />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
@ -0,0 +1,104 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const wrapper = style({
|
||||
width: '100%',
|
||||
});
|
||||
export const recurringRadioGroup = style({
|
||||
width: '256px',
|
||||
});
|
||||
|
||||
export const radioButtonDiscount = style({
|
||||
marginLeft: '4px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
});
|
||||
|
||||
export const planCardsWrapper = style({
|
||||
marginTop: '24px',
|
||||
display: 'flex',
|
||||
overflowX: 'auto',
|
||||
});
|
||||
|
||||
export const planCard = style({
|
||||
minHeight: '426px',
|
||||
minWidth: '258px',
|
||||
borderRadius: '16px',
|
||||
padding: '20px',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
|
||||
selectors: {
|
||||
'&:not(:last-child)': {
|
||||
marginRight: '16px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const currentPlanCard = style([
|
||||
planCard,
|
||||
{
|
||||
borderWidth: '2px',
|
||||
borderColor: 'var(--affine-primary-color)',
|
||||
},
|
||||
]);
|
||||
|
||||
export const discountLabel = style({
|
||||
color: 'var(--affine-primary-color)',
|
||||
marginLeft: '8px',
|
||||
lineHeight: '20px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 500,
|
||||
padding: '0 4px',
|
||||
backgroundColor: 'var(--affine-blue-50)',
|
||||
borderRadius: '4px',
|
||||
display: 'inline-block',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export const planTitle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px',
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
export const planPrice = style({
|
||||
fontSize: 'var(--affine-font-h-5)',
|
||||
marginRight: '8px',
|
||||
});
|
||||
|
||||
export const planPriceDesc = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
});
|
||||
|
||||
export const planAction = style({
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const planBenefits = style({
|
||||
marginTop: '20px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
});
|
||||
|
||||
export const planBenefit = style({
|
||||
display: 'flex',
|
||||
selectors: {
|
||||
'&:not(:last-child)': {
|
||||
marginBottom: '8px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const planBenefitIcon = style({
|
||||
display: 'inline-block',
|
||||
marginRight: '8px',
|
||||
});
|
||||
|
||||
export const allPlansLink = style({
|
||||
display: 'block',
|
||||
marginTop: '36px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
background: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
mutation cancelSubscription {
|
||||
cancelSubscription {
|
||||
id
|
||||
status
|
||||
nextBillAt
|
||||
canceledAt
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
mutation checkout($recurring: SubscriptionRecurring!) {
|
||||
checkout(recurring: $recurring)
|
||||
}
|
@ -79,6 +79,22 @@ query allBlobSizes {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const cancelSubscriptionMutation = {
|
||||
id: 'cancelSubscriptionMutation' as const,
|
||||
operationName: 'cancelSubscription',
|
||||
definitionName: 'cancelSubscription',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation cancelSubscription {
|
||||
cancelSubscription {
|
||||
id
|
||||
status
|
||||
nextBillAt
|
||||
canceledAt
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const changeEmailMutation = {
|
||||
id: 'changeEmailMutation' as const,
|
||||
operationName: 'changeEmail',
|
||||
@ -111,6 +127,17 @@ mutation changePassword($token: String!, $newPassword: String!) {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const checkoutMutation = {
|
||||
id: 'checkoutMutation' as const,
|
||||
operationName: 'checkout',
|
||||
definitionName: 'checkout',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation checkout($recurring: SubscriptionRecurring!) {
|
||||
checkout(recurring: $recurring)
|
||||
}`,
|
||||
};
|
||||
|
||||
export const createWorkspaceMutation = {
|
||||
id: 'createWorkspaceMutation' as const,
|
||||
operationName: 'createWorkspace',
|
||||
@ -321,6 +348,29 @@ query getWorkspaces {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const invoicesQuery = {
|
||||
id: 'invoicesQuery' as const,
|
||||
operationName: 'invoices',
|
||||
definitionName: 'currentUser',
|
||||
containsFile: false,
|
||||
query: `
|
||||
query invoices($take: Int!, $skip: Int!) {
|
||||
currentUser {
|
||||
invoices(take: $take, skip: $skip) {
|
||||
id
|
||||
status
|
||||
plan
|
||||
recurring
|
||||
currency
|
||||
amount
|
||||
reason
|
||||
lastPaymentError
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const leaveWorkspaceMutation = {
|
||||
id: 'leaveWorkspaceMutation' as const,
|
||||
operationName: 'leaveWorkspace',
|
||||
@ -336,6 +386,23 @@ mutation leaveWorkspace($workspaceId: String!, $workspaceName: String!, $sendLea
|
||||
}`,
|
||||
};
|
||||
|
||||
export const pricesQuery = {
|
||||
id: 'pricesQuery' as const,
|
||||
operationName: 'prices',
|
||||
definitionName: 'prices',
|
||||
containsFile: false,
|
||||
query: `
|
||||
query prices {
|
||||
prices {
|
||||
type
|
||||
plan
|
||||
currency
|
||||
amount
|
||||
yearlyAmount
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const removeAvatarMutation = {
|
||||
id: 'removeAvatarMutation' as const,
|
||||
operationName: 'removeAvatar',
|
||||
@ -469,6 +536,44 @@ mutation signUp($name: String!, $email: String!, $password: String!) {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const subscriptionQuery = {
|
||||
id: 'subscriptionQuery' as const,
|
||||
operationName: 'subscription',
|
||||
definitionName: 'currentUser',
|
||||
containsFile: false,
|
||||
query: `
|
||||
query subscription {
|
||||
currentUser {
|
||||
subscription {
|
||||
id
|
||||
status
|
||||
plan
|
||||
recurring
|
||||
start
|
||||
end
|
||||
nextBillAt
|
||||
canceledAt
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const updateSubscriptionMutation = {
|
||||
id: 'updateSubscriptionMutation' as const,
|
||||
operationName: 'updateSubscription',
|
||||
definitionName: 'updateSubscriptionRecurring',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation updateSubscription($recurring: SubscriptionRecurring!) {
|
||||
updateSubscriptionRecurring(recurring: $recurring) {
|
||||
id
|
||||
plan
|
||||
recurring
|
||||
nextBillAt
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const uploadAvatarMutation = {
|
||||
id: 'uploadAvatarMutation' as const,
|
||||
operationName: 'uploadAvatar',
|
||||
|
15
packages/frontend/graphql/src/graphql/invoices.gql
Normal file
15
packages/frontend/graphql/src/graphql/invoices.gql
Normal file
@ -0,0 +1,15 @@
|
||||
query invoices($take: Int!, $skip: Int!) {
|
||||
currentUser {
|
||||
invoices(take: $take, skip: $skip) {
|
||||
id
|
||||
status
|
||||
plan
|
||||
recurring
|
||||
currency
|
||||
amount
|
||||
reason
|
||||
lastPaymentError
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
9
packages/frontend/graphql/src/graphql/prices.gql
Normal file
9
packages/frontend/graphql/src/graphql/prices.gql
Normal file
@ -0,0 +1,9 @@
|
||||
query prices {
|
||||
prices {
|
||||
type
|
||||
plan
|
||||
currency
|
||||
amount
|
||||
yearlyAmount
|
||||
}
|
||||
}
|
14
packages/frontend/graphql/src/graphql/subscription.gql
Normal file
14
packages/frontend/graphql/src/graphql/subscription.gql
Normal file
@ -0,0 +1,14 @@
|
||||
query subscription {
|
||||
currentUser {
|
||||
subscription {
|
||||
id
|
||||
status
|
||||
plan
|
||||
recurring
|
||||
start
|
||||
end
|
||||
nextBillAt
|
||||
canceledAt
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
mutation updateSubscription($recurring: SubscriptionRecurring!) {
|
||||
updateSubscriptionRecurring(recurring: $recurring) {
|
||||
id
|
||||
plan
|
||||
recurring
|
||||
nextBillAt
|
||||
}
|
||||
}
|
@ -130,6 +130,21 @@ export type AllBlobSizesQuery = {
|
||||
collectAllBlobSizes: { __typename?: 'WorkspaceBlobSizes'; size: number };
|
||||
};
|
||||
|
||||
export type CancelSubscriptionMutationVariables = Exact<{
|
||||
[key: string]: never;
|
||||
}>;
|
||||
|
||||
export type CancelSubscriptionMutation = {
|
||||
__typename?: 'Mutation';
|
||||
cancelSubscription: {
|
||||
__typename?: 'UserSubscription';
|
||||
id: string;
|
||||
status: SubscriptionStatus;
|
||||
nextBillAt: string | null;
|
||||
canceledAt: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type ChangeEmailMutationVariables = Exact<{
|
||||
token: Scalars['String']['input'];
|
||||
}>;
|
||||
@ -161,6 +176,12 @@ export type ChangePasswordMutation = {
|
||||
};
|
||||
};
|
||||
|
||||
export type CheckoutMutationVariables = Exact<{
|
||||
recurring: SubscriptionRecurring;
|
||||
}>;
|
||||
|
||||
export type CheckoutMutation = { __typename?: 'Mutation'; checkout: string };
|
||||
|
||||
export type CreateWorkspaceMutationVariables = Exact<{
|
||||
init: Scalars['Upload']['input'];
|
||||
}>;
|
||||
@ -328,6 +349,30 @@ export type GetWorkspacesQuery = {
|
||||
workspaces: Array<{ __typename?: 'WorkspaceType'; id: string }>;
|
||||
};
|
||||
|
||||
export type InvoicesQueryVariables = Exact<{
|
||||
take: Scalars['Int']['input'];
|
||||
skip: Scalars['Int']['input'];
|
||||
}>;
|
||||
|
||||
export type InvoicesQuery = {
|
||||
__typename?: 'Query';
|
||||
currentUser: {
|
||||
__typename?: 'UserType';
|
||||
invoices: Array<{
|
||||
__typename?: 'UserInvoice';
|
||||
id: string;
|
||||
status: InvoiceStatus;
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
currency: string;
|
||||
amount: number;
|
||||
reason: string;
|
||||
lastPaymentError: string | null;
|
||||
createdAt: string;
|
||||
}>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type LeaveWorkspaceMutationVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
workspaceName: Scalars['String']['input'];
|
||||
@ -339,6 +384,20 @@ export type LeaveWorkspaceMutation = {
|
||||
leaveWorkspace: boolean;
|
||||
};
|
||||
|
||||
export type PricesQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type PricesQuery = {
|
||||
__typename?: 'Query';
|
||||
prices: Array<{
|
||||
__typename?: 'SubscriptionPrice';
|
||||
type: string;
|
||||
plan: SubscriptionPlan;
|
||||
currency: string;
|
||||
amount: number;
|
||||
yearlyAmount: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type RemoveAvatarMutation = {
|
||||
@ -451,6 +510,41 @@ export type SignUpMutation = {
|
||||
};
|
||||
};
|
||||
|
||||
export type SubscriptionQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type SubscriptionQuery = {
|
||||
__typename?: 'Query';
|
||||
currentUser: {
|
||||
__typename?: 'UserType';
|
||||
subscription: {
|
||||
__typename?: 'UserSubscription';
|
||||
id: string;
|
||||
status: SubscriptionStatus;
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
start: string;
|
||||
end: string;
|
||||
nextBillAt: string | null;
|
||||
canceledAt: string | null;
|
||||
} | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type UpdateSubscriptionMutationVariables = Exact<{
|
||||
recurring: SubscriptionRecurring;
|
||||
}>;
|
||||
|
||||
export type UpdateSubscriptionMutation = {
|
||||
__typename?: 'Mutation';
|
||||
updateSubscriptionRecurring: {
|
||||
__typename?: 'UserSubscription';
|
||||
id: string;
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
nextBillAt: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type UploadAvatarMutationVariables = Exact<{
|
||||
avatar: Scalars['Upload']['input'];
|
||||
}>;
|
||||
@ -570,6 +664,21 @@ export type Queries =
|
||||
name: 'getWorkspacesQuery';
|
||||
variables: GetWorkspacesQueryVariables;
|
||||
response: GetWorkspacesQuery;
|
||||
}
|
||||
| {
|
||||
name: 'invoicesQuery';
|
||||
variables: InvoicesQueryVariables;
|
||||
response: InvoicesQuery;
|
||||
}
|
||||
| {
|
||||
name: 'pricesQuery';
|
||||
variables: PricesQueryVariables;
|
||||
response: PricesQuery;
|
||||
}
|
||||
| {
|
||||
name: 'subscriptionQuery';
|
||||
variables: SubscriptionQueryVariables;
|
||||
response: SubscriptionQuery;
|
||||
};
|
||||
|
||||
export type Mutations =
|
||||
@ -583,6 +692,11 @@ export type Mutations =
|
||||
variables: SetBlobMutationVariables;
|
||||
response: SetBlobMutation;
|
||||
}
|
||||
| {
|
||||
name: 'cancelSubscriptionMutation';
|
||||
variables: CancelSubscriptionMutationVariables;
|
||||
response: CancelSubscriptionMutation;
|
||||
}
|
||||
| {
|
||||
name: 'changeEmailMutation';
|
||||
variables: ChangeEmailMutationVariables;
|
||||
@ -593,6 +707,11 @@ export type Mutations =
|
||||
variables: ChangePasswordMutationVariables;
|
||||
response: ChangePasswordMutation;
|
||||
}
|
||||
| {
|
||||
name: 'checkoutMutation';
|
||||
variables: CheckoutMutationVariables;
|
||||
response: CheckoutMutation;
|
||||
}
|
||||
| {
|
||||
name: 'createWorkspaceMutation';
|
||||
variables: CreateWorkspaceMutationVariables;
|
||||
@ -668,6 +787,11 @@ export type Mutations =
|
||||
variables: SignUpMutationVariables;
|
||||
response: SignUpMutation;
|
||||
}
|
||||
| {
|
||||
name: 'updateSubscriptionMutation';
|
||||
variables: UpdateSubscriptionMutationVariables;
|
||||
response: UpdateSubscriptionMutation;
|
||||
}
|
||||
| {
|
||||
name: 'uploadAvatarMutation';
|
||||
variables: UploadAvatarMutationVariables;
|
||||
|
Loading…
Reference in New Issue
Block a user