first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import {Dialog} from '../../ui/overlays/dialog/dialog';
import {DialogHeader} from '../../ui/overlays/dialog/dialog-header';
import {Trans} from '../../i18n/trans';
import {DialogBody} from '../../ui/overlays/dialog/dialog-body';
import {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';
import {Button} from '../../ui/buttons/button';
import {useDialogContext} from '../../ui/overlays/dialog/dialog-context';
import {useForm} from 'react-hook-form';
import {useCreateSubscription} from './requests/use-create-subscription';
import {Subscription} from '../../billing/subscription';
import {CrupdateSubscriptionForm} from './crupdate-subscription-form';
export function CreateSubscriptionDialog() {
const {close, formId} = useDialogContext();
const form = useForm<Partial<Subscription>>({});
const createSubscription = useCreateSubscription(form);
return (
<Dialog>
<DialogHeader>
<Trans message="Add new subscription" />
</DialogHeader>
<DialogBody>
<CrupdateSubscriptionForm
formId={formId}
form={form}
onSubmit={values => {
createSubscription.mutate(values, {
onSuccess: () => {
close();
},
});
}}
/>
</DialogBody>
<DialogFooter>
<Button
onClick={() => {
close();
}}
>
<Trans message="Cancel" />
</Button>
<Button
form={formId}
disabled={createSubscription.isPending}
variant="flat"
color="primary"
type="submit"
>
<Trans message="Save" />
</Button>
</DialogFooter>
</Dialog>
);
}

View File

@@ -0,0 +1,93 @@
import {UseFormReturn} from 'react-hook-form';
import {Form} from '../../ui/forms/form';
import {FormTextField} from '../../ui/forms/input-field/text-field/text-field';
import {FormSelect} from '../../ui/forms/select/select';
import {Trans} from '../../i18n/trans';
import {Item} from '../../ui/forms/listbox/item';
import {Subscription} from '../../billing/subscription';
import {FormDatePicker} from '../../ui/forms/input-field/date/date-picker/date-picker';
import {useProducts} from '../../billing/pricing-table/use-products';
import {FormattedPrice} from '../../i18n/formatted-price';
import {FormNormalizedModelField} from '../../ui/forms/normalized-model-field';
interface CrupdateSubscriptionForm {
onSubmit: (values: Partial<Subscription>) => void;
formId: string;
form: UseFormReturn<Partial<Subscription>>;
}
export function CrupdateSubscriptionForm({
form,
onSubmit,
formId,
}: CrupdateSubscriptionForm) {
const query = useProducts();
// @ts-ignore
const watchedProductId = form.watch('product_id');
const selectedProduct = query.data?.products.find(
p => p.id === watchedProductId,
);
return (
<Form id={formId} form={form} onSubmit={onSubmit}>
<FormNormalizedModelField
name="user_id"
className="mb-20"
endpoint="normalized-models/user"
label={<Trans message="User" />}
/>
<FormSelect
name="product_id"
selectionMode="single"
className="mb-20"
label={<Trans message="Plan" />}
>
{query.data?.products
.filter(p => !p.free)
.map(product => (
<Item key={product.id} value={product.id}>
<Trans message={product.name} />
</Item>
))}
</FormSelect>
{!selectedProduct?.free && (
<FormSelect
name="price_id"
selectionMode="single"
className="mb-20"
label={<Trans message="Price" />}
>
{selectedProduct?.prices.map(price => (
<Item key={price.id} value={price.id}>
<FormattedPrice price={price} />
</Item>
))}
</FormSelect>
)}
<FormTextField
inputElementType="textarea"
rows={3}
name="description"
label={<Trans message="Description" />}
className="mb-20"
/>
<FormDatePicker
className="mb-20"
name="renews_at"
granularity="day"
label={<Trans message="Renews at" />}
description={
<Trans message="This will only change local records. User will continue to be billed on their original cycle on the payment gateway." />
}
/>
<FormDatePicker
className="mb-20"
name="ends_at"
granularity="day"
label={<Trans message="Ends at" />}
description={
<Trans message="This will only change local records. User will continue to be billed on their original cycle on the payment gateway." />
}
/>
</Form>
);
}

View File

@@ -0,0 +1,37 @@
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '../../../http/query-client';
import {useTrans} from '../../../i18n/use-trans';
import {BackendResponse} from '../../../http/backend-response/backend-response';
import {toast} from '../../../ui/toast/toast';
import {message} from '../../../i18n/message';
import {Tag} from '../../../tags/tag';
import {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';
import {onFormQueryError} from '../../../errors/on-form-query-error';
import {UseFormReturn} from 'react-hook-form';
import {Subscription} from '../../../billing/subscription';
const endpoint = 'billing/subscriptions';
interface Response extends BackendResponse {
tag: Tag;
}
interface Payload extends Partial<Subscription> {}
export function useCreateSubscription(form: UseFormReturn<Payload>) {
const {trans} = useTrans();
return useMutation({
mutationFn: (props: Payload) => createNewSubscription(props),
onSuccess: () => {
toast(trans(message('Subscription created')));
queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey(endpoint),
});
},
onError: err => onFormQueryError(err, form),
});
}
function createNewSubscription(payload: Payload): Promise<Response> {
return apiClient.post(endpoint, payload).then(r => r.data);
}

View File

@@ -0,0 +1,43 @@
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '../../../http/query-client';
import {useTrans} from '../../../i18n/use-trans';
import {BackendResponse} from '../../../http/backend-response/backend-response';
import {toast} from '../../../ui/toast/toast';
import {message} from '../../../i18n/message';
import {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';
import {onFormQueryError} from '../../../errors/on-form-query-error';
import {UseFormReturn} from 'react-hook-form';
import {Subscription} from '../../../billing/subscription';
interface Response extends BackendResponse {
subscription: Subscription;
}
export interface UpdateSubscriptionPayload extends Partial<Subscription> {
id: number;
}
export function useUpdateSubscription(
form: UseFormReturn<UpdateSubscriptionPayload>,
) {
const {trans} = useTrans();
return useMutation({
mutationFn: (props: UpdateSubscriptionPayload) => updateSubscription(props),
onSuccess: () => {
toast(trans(message('Subscription updated')));
queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey('billing/subscriptions'),
});
},
onError: err => onFormQueryError(err, form),
});
}
function updateSubscription({
id,
...payload
}: UpdateSubscriptionPayload): Promise<Response> {
return apiClient
.put(`billing/subscriptions/${id}`, payload)
.then(r => r.data);
}

View File

@@ -0,0 +1,76 @@
import {
BackendFilter,
FilterControlType,
FilterOperator,
} from '../../datatable/filters/backend-filter';
import {message} from '../../i18n/message';
import {
createdAtFilter,
timestampFilter,
updatedAtFilter,
} from '../../datatable/filters/timestamp-filters';
export const SubscriptionIndexPageFilters: BackendFilter[] = [
{
key: 'ends_at',
label: message('Status'),
description: message('Whether subscription is active or cancelled'),
defaultOperator: FilterOperator.eq,
control: {
type: FilterControlType.Select,
defaultValue: 'active',
options: [
{
key: 'active',
label: message('Active'),
value: {value: null, operator: FilterOperator.eq},
},
{
key: 'cancelled',
label: message('Cancelled'),
value: {value: null, operator: FilterOperator.ne},
},
],
},
},
{
control: {
type: FilterControlType.Select,
defaultValue: 'stripe',
options: [
{
key: 'stripe',
label: message('Stripe'),
value: 'stripe',
},
{
key: 'paypal',
label: message('PayPal'),
value: 'paypal',
},
{
key: 'none',
label: message('None'),
value: 'none',
},
],
},
key: 'gateway_name',
label: message('Gateway'),
description: message(
'With which payment provider was subscription created'
),
defaultOperator: FilterOperator.eq,
},
timestampFilter({
key: 'renews_at',
label: message('Renew date'),
description: message('Date subscription will renew'),
}),
createdAtFilter({
description: message('Date subscription was created'),
}),
updatedAtFilter({
description: message('Date subscription was last updated'),
}),
];

View File

@@ -0,0 +1,314 @@
import React, {Fragment} from 'react';
import {DataTablePage} from '../../datatable/page/data-table-page';
import {IconButton} from '../../ui/buttons/icon-button';
import {EditIcon} from '../../icons/material/Edit';
import {ColumnConfig} from '../../datatable/column-config';
import {Trans} from '../../i18n/trans';
import {DeleteSelectedItemsAction} from '../../datatable/page/delete-selected-items-action';
import {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';
import {SubscriptionIndexPageFilters} from './subscription-index-page-filters';
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
import {DataTableAddItemButton} from '../../datatable/data-table-add-item-button';
import subscriptionsSvg from './subscriptions.svg';
import {NameWithAvatar} from '../../datatable/column-templates/name-with-avatar';
import {Subscription} from '../../billing/subscription';
import {CloseIcon} from '../../icons/material/Close';
import {FormattedDate} from '../../i18n/formatted-date';
import {UpdateSubscriptionDialog} from './update-subscription-dialog';
import {CreateSubscriptionDialog} from './create-subscription-dialog';
import {useCancelSubscription} from '../../billing/billing-page/requests/use-cancel-subscription';
import {PauseIcon} from '../../icons/material/Pause';
import {queryClient} from '../../http/query-client';
import {DatatableDataQueryKey} from '../../datatable/requests/paginated-resources';
import {Tooltip} from '../../ui/tooltip/tooltip';
import {useResumeSubscription} from '../../billing/billing-page/requests/use-resume-subscription';
import {PlayArrowIcon} from '../../icons/material/PlayArrow';
import {ConfirmationDialog} from '../../ui/overlays/dialog/confirmation-dialog';
import {Chip} from '../../ui/forms/input-field/chip-field/chip';
const endpoint = 'billing/subscriptions';
const columnConfig: ColumnConfig<Subscription>[] = [
{
key: 'user_id',
allowsSorting: true,
width: 'flex-3 min-w-200',
visibleInMode: 'all',
header: () => <Trans message="Customer" />,
body: subscription =>
subscription.user && (
<NameWithAvatar
image={subscription.user.avatar}
label={subscription.user.display_name}
description={subscription.user.email}
/>
),
},
{
key: 'status',
width: 'w-100 flex-shrink-0',
header: () => <Trans message="Status" />,
body: subscription => (
<Chip
size="xs"
color={subscription.valid ? 'positive' : undefined}
radius="rounded"
className="w-max"
>
{subscription.gateway_status}
</Chip>
),
},
{
key: 'product_id',
allowsSorting: true,
header: () => <Trans message="Plan" />,
body: subscription => subscription.product?.name,
},
{
key: 'gateway_name',
allowsSorting: true,
header: () => <Trans message="Gateway" />,
body: subscription => (
<span className="capitalize">{subscription.gateway_name}</span>
),
},
{
key: 'renews_at',
allowsSorting: true,
header: () => <Trans message="Renews at" />,
body: subscription => <FormattedDate date={subscription.renews_at} />,
},
{
key: 'ends_at',
allowsSorting: true,
header: () => <Trans message="Ends at" />,
body: subscription => <FormattedDate date={subscription.ends_at} />,
},
{
key: 'created_at',
allowsSorting: true,
header: () => <Trans message="Created at" />,
body: subscription => <FormattedDate date={subscription.created_at} />,
},
{
key: 'actions',
header: () => <Trans message="Actions" />,
hideHeader: true,
align: 'end',
visibleInMode: 'all',
width: 'w-[168px] flex-shrink-0',
body: subscription => {
return <SubscriptionActions subscription={subscription} />;
},
},
];
export function SubscriptionsIndexPage() {
return (
<DataTablePage
endpoint={endpoint}
title={<Trans message="Subscriptions" />}
columns={columnConfig}
filters={SubscriptionIndexPageFilters}
actions={<PageActions />}
enableSelection={false}
selectedActions={<DeleteSelectedItemsAction />}
queryParams={{with: 'product'}}
emptyStateMessage={
<DataTableEmptyStateMessage
image={subscriptionsSvg}
title={<Trans message="No subscriptions have been created yet" />}
filteringTitle={<Trans message="No matching subscriptions" />}
/>
}
/>
);
}
function PageActions() {
return (
<>
<DialogTrigger type="modal">
<DataTableAddItemButton>
<Trans message="Add new subscription" />
</DataTableAddItemButton>
<CreateSubscriptionDialog />
</DialogTrigger>
</>
);
}
interface SubscriptionActionsProps {
subscription: Subscription;
}
function SubscriptionActions({subscription}: SubscriptionActionsProps) {
return (
<Fragment>
<DialogTrigger type="modal">
<IconButton size="md" className="text-muted">
<EditIcon />
</IconButton>
<UpdateSubscriptionDialog subscription={subscription} />
</DialogTrigger>
{subscription.cancelled && subscription.on_grace_period ? (
<ResumeSubscriptionButton subscription={subscription} />
) : null}
{subscription.active ? (
<SuspendSubscriptionButton subscription={subscription} />
) : null}
<CancelSubscriptionButton subscription={subscription} />
</Fragment>
);
}
function SuspendSubscriptionButton({subscription}: SubscriptionActionsProps) {
const cancelSubscription = useCancelSubscription();
const handleSuspendSubscription = () => {
cancelSubscription.mutate(
{subscriptionId: subscription.id},
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey(endpoint),
});
},
},
);
};
return (
<DialogTrigger
type="modal"
onClose={confirmed => {
if (confirmed) {
handleSuspendSubscription();
}
}}
>
<Tooltip label={<Trans message="Cancel subscription" />}>
<IconButton
size="md"
className="text-muted"
disabled={cancelSubscription.isPending}
>
<PauseIcon />
</IconButton>
</Tooltip>
<ConfirmationDialog
title={<Trans message="Cancel subscription" />}
body={
<div>
<Trans message="Are you sure you want to cancel this subscription?" />
<div className="mt-10 text-sm font-semibold">
<Trans message="This will put user on grace period until their next scheduled renewal date. Subscription can be renewed until that date by user or from admin area." />
</div>
</div>
}
confirm={<Trans message="Confirm" />}
/>
</DialogTrigger>
);
}
function ResumeSubscriptionButton({subscription}: SubscriptionActionsProps) {
const resumeSubscription = useResumeSubscription();
const handleResumeSubscription = () => {
resumeSubscription.mutate(
{subscriptionId: subscription.id},
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey(endpoint),
});
},
},
);
};
return (
<DialogTrigger
type="modal"
onClose={confirmed => {
if (confirmed) {
handleResumeSubscription();
}
}}
>
<Tooltip label={<Trans message="Renew subscription" />}>
<IconButton
size="md"
className="text-muted"
onClick={handleResumeSubscription}
disabled={resumeSubscription.isPending}
>
<PlayArrowIcon />
</IconButton>
</Tooltip>
<ConfirmationDialog
title={<Trans message="Resume subscription" />}
body={
<div>
<Trans message="Are you sure you want to resume this subscription?" />
<div className="mt-10 text-sm font-semibold">
<Trans message="This will put user on their original plan and billing cycle." />
</div>
</div>
}
confirm={<Trans message="Confirm" />}
/>
</DialogTrigger>
);
}
function CancelSubscriptionButton({subscription}: SubscriptionActionsProps) {
const cancelSubscription = useCancelSubscription();
const handleDeleteSubscription = () => {
cancelSubscription.mutate(
{subscriptionId: subscription.id, delete: true},
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey(endpoint),
});
},
},
);
};
return (
<DialogTrigger
type="modal"
onClose={confirmed => {
if (confirmed) {
handleDeleteSubscription();
}
}}
>
<Tooltip label={<Trans message="Delete subscription" />}>
<IconButton
size="md"
className="text-muted"
disabled={cancelSubscription.isPending}
>
<CloseIcon />
</IconButton>
</Tooltip>
<ConfirmationDialog
isDanger
title={<Trans message="Delete subscription" />}
body={
<div>
<Trans message="Are you sure you want to delete this subscription?" />
<div className="mt-10 text-sm font-semibold">
<Trans message="This will permanently delete the subscription and immediately cancel it on billing gateway. Subscription will not be renewable anymore." />
</div>
</div>
}
confirm={<Trans message="Confirm" />}
/>
</DialogTrigger>
);
}

View File

@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" width="733.82" height="503.768" viewBox="0 0 733.82 503.768" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Group_16" data-name="Group 16" transform="translate(-196.555 -165.086)">
<path id="Path_204-133" data-name="Path 204" d="M261.846,378.459a45.126,45.126,0,1,1,0-90.252H672.56a45.126,45.126,0,1,1,0,90.252Z" transform="translate(-20.165 -123.12)" fill="#e6e6e6"/>
<path id="Path_205-134" data-name="Path 205" d="M264.96,297.207a39.24,39.24,0,0,0,0,78.48H675.674a39.24,39.24,0,1,0,0-78.48Z" transform="translate(-23.279 -126.234)" fill="#fff"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="1.308" height="78.48" transform="translate(364.961 170.972)" fill="#e6e6e6"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="1.308" height="78.48" transform="translate(527.808 170.972)" fill="#e6e6e6"/>
<rect id="Rectangle_7" data-name="Rectangle 7" width="161.539" height="78.48" transform="translate(366.269 170.972)" fill="rgb(var(--be-primary))"/>
<path id="Path_198-135" data-name="Path 198" d="M276.268,206.815a24,24,0,1,0,24,24,24,24,0,0,0-24-24Zm0,7.2a7.2,7.2,0,1,1-7.2,7.2A7.2,7.2,0,0,1,276.268,214.016Zm0,34.662a17.484,17.484,0,0,1-14.4-7.685c.115-4.8,9.6-7.442,14.4-7.442s14.285,2.642,14.4,7.442a17.513,17.513,0,0,1-14.4,7.685Z" transform="translate(170.844 -20.53)" fill="#fff"/>
<path id="Path_200-136" data-name="Path 200" d="M620.7,206.815a24,24,0,1,0,24,24,24,24,0,0,0-24-24Zm0,7.2a7.2,7.2,0,1,1-7.2,7.2A7.2,7.2,0,0,1,620.7,214.015Zm0,34.662a17.484,17.484,0,0,1-14.4-7.685c.115-4.8,9.6-7.442,14.4-7.442s14.285,2.642,14.4,7.442a17.513,17.513,0,0,1-14.4,7.685Z" transform="translate(-336.439 -20.53)" fill="#e6e6e6"/>
<path id="Path_242-137" data-name="Path 242" d="M620.7,206.815a24,24,0,1,0,24,24,24,24,0,0,0-24-24Zm0,7.2a7.2,7.2,0,1,1-7.2,7.2A7.2,7.2,0,0,1,620.7,214.015Zm0,34.662a17.484,17.484,0,0,1-14.4-7.685c.115-4.8,9.6-7.442,14.4-7.442s14.285,2.642,14.4,7.442a17.513,17.513,0,0,1-14.4,7.685Z" transform="translate(-10.892 -20.53)" fill="#e6e6e6"/>
<rect id="Rectangle_9" data-name="Rectangle 9" width="56" height="56" rx="6" transform="translate(419.112 288.229)" fill="rgb(var(--be-primary))"/>
<ellipse id="Ellipse_29" data-name="Ellipse 29" cx="134.439" cy="18" rx="134.439" ry="18" transform="translate(661.497 632.854)" fill="#e6e6e6"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="56" height="56" rx="6" transform="translate(581.812 288.049)" fill="#e6e6e6"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="40.798" height="40.798" transform="translate(589.812 295.83)" fill="#fff"/>
<path id="Path_202-138" data-name="Path 202" d="M253.345,218.766l-7.075-9.1,4.114-3.2,3.35,4.307,11.318-11.946,3.785,3.585Z" transform="translate(191.14 106.158)" fill="#fff"/>
<path id="Path_203-139" data-name="Path 203" d="M425.345,218.766l-7.075-9.1,4.114-3.2,3.35,4.307,11.317-11.946,3.785,3.585Z" transform="translate(182.106 106.158)" fill="#e6e6e6"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="56" height="56" rx="6" transform="translate(256.265 288.049)" fill="#e6e6e6"/>
<rect id="Rectangle_19" data-name="Rectangle 19" width="40.798" height="40.798" transform="translate(264.265 295.83)" fill="#fff"/>
<path id="Path_243-140" data-name="Path 243" d="M425.345,218.766l-7.075-9.1,4.114-3.2,3.35,4.307,11.317-11.946,3.785,3.585Z" transform="translate(-143.441 106.158)" fill="#e6e6e6"/>
<g id="Group_15" data-name="Group 15">
<path id="Path_257-141" data-name="Path 257" d="M340.66,397.363H327.48l-6.268-50.837,19.452,0Z" transform="translate(545.904 239.259)" fill="#ffb8b8"/>
<path id="Path_258-142" data-name="Path 258" d="M320.6,387.355h25.418v16H304.6a16,16,0,0,1,16-16Z" transform="translate(543.364 245.5)" fill="#2f2e41"/>
<path id="Path_259-143" data-name="Path 259" d="M223.865,397.363h-13.18l-6.268-50.837,19.452,0Z" transform="translate(528.049 239.259)" fill="#ffb8b8"/>
<path id="Path_260-144" data-name="Path 260" d="M203.81,387.355h25.418v16H187.806a16,16,0,0,1,16-16Z" transform="translate(525.51 245.5)" fill="#2f2e41"/>
<path id="Path_261-145" data-name="Path 261" d="M487.471,249.585V243.82a37.18,37.18,0,0,1,37.18-37.18h0a37.18,37.18,0,0,1,37.18,37.18v5.764a26.8,26.8,0,0,1-26.8,26.8H514.275a26.8,26.8,0,0,1-26.8-26.8Z" transform="translate(308.465 9.946)" fill="#2f2e41"/>
<ellipse id="Ellipse_36" data-name="Ellipse 36" cx="28.316" cy="28.316" rx="28.316" ry="28.316" transform="translate(804.801 231.687)" fill="#ffb8b8"/>
<path id="Path_263-146" data-name="Path 263" d="M386.583,329.1a10.811,10.811,0,0,1,16.463,1.934l24.273-4.591,6.388,14.07-34.37,6A10.869,10.869,0,0,1,386.583,329.1Z" transform="translate(292.514 28.216)" fill="#ffb8b8"/>
<path id="Path_264-147" data-name="Path 264" d="M515.087,284.516l.317.481-39.8,26.221-67.164,21.447a4.044,4.044,0,0,0-2.781,4.31l1.465,12.62a4.036,4.036,0,0,0,4.854,3.48l63.212-13.549a22.833,22.833,0,0,0,8.5-3.742L528.4,303.969A11.5,11.5,0,0,0,515.4,285Z" transform="translate(295.954 21.634)" fill="#ccc"/>
<path id="Path_265-148" data-name="Path 265" d="M574.076,590.876a5.209,5.209,0,0,1-4.771-3.115l-60.421-149.3a1.729,1.729,0,0,0-3.238.182L456.351,583.993a5.189,5.189,0,0,1-6.781,3.333l-16.53-6.2a5.175,5.175,0,0,1-3.34-4.271c-7.437-64.782,57.413-228.3,58.069-229.946l.182-.455,59.116,13.077.123.134c23.585,25.73,42.971,188.012,46.618,220.283a5.163,5.163,0,0,1-3.425,5.472l-14.591,5.16a5.139,5.139,0,0,1-1.716.295Z" transform="translate(299.543 31.32)" fill="#2f2e41"/>
<path id="Path_266-149" data-name="Path 266" d="M515.547,375.9c-14.323,0-30.291-2.856-35.206-14.642l-.113-.271.153-.251c3.88-6.366,9.007-17.224,6.251-19.263-5.429-4.014-8.064-10.618-7.83-19.628.508-19.559,13.835-36.925,33.163-43.212h0a147.146,147.146,0,0,1,16.443-4.234,27.993,27.993,0,0,1,23.21,5.732,28.276,28.276,0,0,1,10.486,21.755c.2,20.9-3.015,50.015-19.5,70a5.128,5.128,0,0,1-3.036,1.765A140.9,140.9,0,0,1,515.547,375.9Z" transform="translate(307.138 20.219)" fill="#ccc"/>
<path id="Path_267-150" data-name="Path 267" d="M506.106,364.845a11.017,11.017,0,0,1,13.464-7.683,10.843,10.843,0,0,1,1.669.618l18.43-16.773,12.818,8.635L526.13,372.966a11,11,0,0,1-12.466,5.288,10.83,10.83,0,0,1-7.558-13.409Z" transform="translate(311.251 30.487)" fill="#ffb8b8"/>
<path id="Path_268-151" data-name="Path 268" d="M534.283,373.874A5.174,5.174,0,0,1,531,372.7l-7.268-5.939a5.188,5.188,0,0,1,.126-8.134l30.484-23.38a1.733,1.733,0,0,0,.327-2.415l-18.815-24.875a15.316,15.316,0,0,1,1.023-19.731h0a15.273,15.273,0,0,1,20.622-1.649l.119.126,19.647,28.133a17.515,17.515,0,0,1-.415,27.883l-39.481,30.134a5.2,5.2,0,0,1-3.088,1.017Z" transform="translate(313.718 21.67)" fill="#ccc"/>
<path id="Path_269-152" data-name="Path 269" d="M497.965,240.705V226.656L523.047,215.7l23.916,10.952v14.049a2.306,2.306,0,0,1-2.306,2.306H500.271a2.306,2.306,0,0,1-2.306-2.306Z" transform="translate(310.07 11.332)" fill="#2f2e41"/>
<circle id="Ellipse_30" data-name="Ellipse 30" cx="15.722" cy="15.722" r="15.722" transform="translate(838.852 199.377)" fill="#2f2e41"/>
<path id="Path_185-153" data-name="Path 185" d="M896.5,218.806a15.715,15.715,0,0,1,18.8-15.417,15.715,15.715,0,1,0-9.764,29.629,15.709,15.709,0,0,1-9.032-14.212Z" transform="translate(-56.438 -12.141)" fill="#2f2e41"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,74 @@
import {Dialog} from '../../ui/overlays/dialog/dialog';
import {DialogHeader} from '../../ui/overlays/dialog/dialog-header';
import {Trans} from '../../i18n/trans';
import {DialogBody} from '../../ui/overlays/dialog/dialog-body';
import {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';
import {Button} from '../../ui/buttons/button';
import {useDialogContext} from '../../ui/overlays/dialog/dialog-context';
import {useForm} from 'react-hook-form';
import {Subscription} from '../../billing/subscription';
import {
UpdateSubscriptionPayload,
useUpdateSubscription,
} from './requests/use-update-subscription';
import {CrupdateSubscriptionForm} from './crupdate-subscription-form';
interface UpdateSubscriptionDialogProps {
subscription: Subscription;
}
export function UpdateSubscriptionDialog({
subscription,
}: UpdateSubscriptionDialogProps) {
const {close, formId} = useDialogContext();
const form = useForm<UpdateSubscriptionPayload>({
defaultValues: {
id: subscription.id,
product_id: subscription.product_id,
price_id: subscription.price_id,
description: subscription.description,
renews_at: subscription.renews_at,
ends_at: subscription.ends_at,
user_id: subscription.user_id,
},
});
const updateSubscription = useUpdateSubscription(form);
return (
<Dialog size="md">
<DialogHeader>
<Trans message="Update subscription" />
</DialogHeader>
<DialogBody>
<CrupdateSubscriptionForm
formId={formId}
form={form as any}
onSubmit={values => {
updateSubscription.mutate(values as UpdateSubscriptionPayload, {
onSuccess: () => {
close();
},
});
}}
/>
</DialogBody>
<DialogFooter>
<Button
onClick={() => {
close();
}}
>
<Trans message="Cancel" />
</Button>
<Button
form={formId}
disabled={updateSubscription.isPending}
variant="flat"
color="primary"
type="submit"
>
<Trans message="Save" />
</Button>
</DialogFooter>
</Dialog>
);
}