Compare commits

...

7 Commits

Author SHA1 Message Date
Szilárd Dóró
c625317342 Merge pull request #1841 from nhost/changeset-release/main
chore: update versions
2023-04-17 08:36:12 +02:00
github-actions[bot]
8ab75a4146 chore: update versions 2023-04-14 09:54:44 +00:00
Szilárd Dóró
607f465616 Merge pull request #1840 from nhost/chore/use-dialog-hook
chore(dashboard): unify payment dialog management
2023-04-14 11:50:15 +02:00
Szilárd Dóró
668c877130 chore: add changeset 2023-04-14 11:17:32 +02:00
Szilárd Dóró
4bd870eb96 chore: relocate BillingPaymentMethodForm 2023-04-14 11:15:58 +02:00
Szilárd Dóró
39b3161d91 fix: use up-to-date card information 2023-04-14 10:31:27 +02:00
Szilárd Dóró
ae090a6585 chore: unify modal management for payments 2023-04-13 16:53:30 +02:00
17 changed files with 131 additions and 169 deletions

View File

@@ -1,5 +1,11 @@
# @nhost/dashboard
## 0.14.8
### Patch Changes
- 668c8771: chore(dialogs): unify dialog management of payment dialogs
## 0.14.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.14.7",
"version": "0.14.8",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View File

@@ -124,13 +124,9 @@ export default function ApplicationPaused() {
className="mx-auto w-full max-w-[280px]"
onClick={() => {
openDialog({
title: 'Upgrade your plan.',
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0' },
hidePrimaryAction: true,
hideSecondaryAction: true,
hideTitle: true,
maxWidth: 'lg',
},
});

View File

@@ -1,6 +1,5 @@
import { BillingPaymentMethodForm } from '@/components/billing-payment-method/BillingPaymentMethodForm';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/context/UIContext';
import { BillingPaymentMethodForm } from '@/components/workspace/BillingPaymentMethodForm';
import {
refetchGetApplicationPlanQuery,
useGetAppPlanAndGlobalPlansQuery,
@@ -8,18 +7,19 @@ import {
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { Modal } from '@/ui/Modal';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Checkbox from '@/ui/v2/Checkbox';
import { BaseDialog } from '@/ui/v2/Dialog';
import Text from '@/ui/v2/Text';
import { planDescriptions } from '@/utils/planDescriptions';
import { triggerToast } from '@/utils/toast';
import { useTheme } from '@mui/material';
import getServerError from '@/utils/settings/getServerError/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
function Plan({
planName,
@@ -66,13 +66,15 @@ function Plan({
}
export function ChangePlanModalWithData({ app, plans, close }: any) {
const theme = useTheme();
const [selectedPlanId, setSelectedPlanId] = useState('');
const { closeAlertDialog } = useDialog();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const {
currentWorkspace,
currentProject,
refetch: refetchWorkspaceAndProject,
} = useCurrentWorkspaceAndProject();
// get workspace payment methods
const { data } = useGetPaymentMethodsQuery({
variables: {
workspaceId: currentWorkspace?.id,
@@ -80,7 +82,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
skip: !currentWorkspace,
});
const { openPaymentModal, closePaymentModal, paymentModal } = useUI();
const [showPaymentModal, setShowPaymentModal] = useState(false);
const paymentMethodAvailable = data?.paymentMethods.length > 0;
const currentPlan = plans.find((plan) => plan.id === app.plan.id);
@@ -88,7 +90,6 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
const isDowngrade = currentPlan.price > selectedPlan?.price;
// graphql mutations
const [updateApp] = useUpdateApplicationMutation({
refetchQueries: [
refetchGetApplicationPlanQuery({
@@ -98,28 +99,35 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
],
});
// function handlers
const handleUpdateAppPlan = async () => {
await updateApp({
variables: {
appId: app.id,
app: {
planId: selectedPlan.id,
try {
await toast.promise(
updateApp({
variables: {
appId: app.id,
app: {
planId: selectedPlan.id,
},
},
}),
{
loading: 'Updating plan...',
success: `Plan has been updated successfully to ${selectedPlan.name}.`,
error: getServerError(
'An error occurred while updating the plan. Please try again.',
),
},
},
});
getToastStyleProps(),
);
if (isDowngrade) {
if (close) {
close();
}
await refetchWorkspaceAndProject();
close?.();
closeAlertDialog();
setShowPaymentModal(false);
} catch (error) {
// Note: Error is handled by the toast.
}
triggerToast(
`${currentProject.name} plan changed to ${selectedPlan.name}.`,
);
};
const handleChangePlanClick = async () => {
@@ -128,33 +136,30 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
}
if (!paymentMethodAvailable) {
openPaymentModal();
setShowPaymentModal(true);
return;
}
await handleUpdateAppPlan();
if (close) {
close();
}
setShowPaymentModal(false);
close?.();
closeAlertDialog();
};
return (
<Box className="w-full max-w-xl rounded-lg p-6 text-left">
<Modal
showModal={paymentModal}
close={closePaymentModal}
dialogStyle={{ zIndex: theme.zIndex.modal + 1 }}
<BaseDialog
open={showPaymentModal}
onClose={() => setShowPaymentModal(false)}
>
<BillingPaymentMethodForm
close={closePaymentModal}
onPaymentMethodAdded={handleUpdateAppPlan}
workspaceId={currentWorkspace.id}
/>
</Modal>
</BaseDialog>
<div className="flex flex-col">
<div className="mx-auto">
<Image
@@ -217,14 +222,12 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
export interface ChangePlanModalProps {
/**
* Function to close the modal if mounted on parent component.
*
* @deprecated Implement modal by using `openAlertDialog` hook instead.
* Function to close the modal.
*/
close?: () => void;
onCancel?: () => void;
}
export function ChangePlanModal({ close }: ChangePlanModalProps) {
export function ChangePlanModal({ onCancel }: ChangePlanModalProps) {
const {
query: { workspaceSlug, appSlug },
} = useRouter();
@@ -250,5 +253,5 @@ export function ChangePlanModal({ close }: ChangePlanModalProps) {
const { apps, plans } = data;
const app = apps[0];
return <ChangePlanModalWithData app={app} plans={plans} close={close} />;
return <ChangePlanModalWithData app={app} plans={plans} close={onCancel} />;
}

View File

@@ -30,13 +30,9 @@ export function UnlockFeatureByUpgrading({
variant="borderless"
onClick={() => {
openDialog({
title: 'Upgrade your plan.',
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0 max-w-xl w-full' },
hidePrimaryAction: true,
hideSecondaryAction: true,
hideTitle: true,
},
});
}}

View File

@@ -22,7 +22,7 @@ export interface OpenDialogOptions {
/**
* Title of the dialog.
*/
title: ReactNode;
title?: ReactNode;
/**
* Component to render inside the dialog skeleton.
*/

View File

@@ -22,6 +22,13 @@ export function CountrySelector({ value, onChange }: CountrySelectorProps) {
value={value || null}
onChange={(_event, inputValue) => onChange(inputValue as string)}
placeholder="Select Country"
slotProps={{
listbox: { className: 'min-w-0 w-full' },
popper: {
disablePortal: false,
className: 'z-[10000] w-[270px] w-full',
},
}}
>
{countries?.map((country) => (
<Option key={country.name} value={country.code}>

View File

@@ -93,13 +93,9 @@ export default function OverviewTopBar() {
className="mr-2"
onClick={() => {
openDialog({
title: 'Upgrade your plan.',
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0 max-w-xl w-full' },
hidePrimaryAction: true,
hideSecondaryAction: true,
hideTitle: true,
},
});
}}

View File

@@ -42,7 +42,7 @@ function AlertDialog({
}}
{...props}
>
{!hideTitle && (
{!hideTitle && !!title && (
<Dialog.Title {...titleProps} id="alert-dialog-title">
{title}
</Dialog.Title>

View File

@@ -22,7 +22,7 @@ function Dialog({
aria-describedby="dialog-description"
{...props}
>
{!hideTitle && (
{!hideTitle && !!title && (
<DialogTitle
sx={{
padding: (theme) => theme.spacing(3, 3, 1.5, 3),

View File

@@ -16,7 +16,7 @@ export interface CommonDialogProps
/**
* The title of the dialog.
*/
title: ReactNode;
title?: ReactNode;
/**
* The message to display in the dialog.
*/

View File

@@ -27,13 +27,11 @@ const stripePromise = process.env.NEXT_PUBLIC_STRIPE_PK
: null;
type AddPaymentMethodFormProps = {
close: () => void;
onPaymentMethodAdded?: () => Promise<void>;
onPaymentMethodAdded?: () => void;
workspaceId: string;
};
function AddPaymentMethodForm({
close,
onPaymentMethodAdded,
workspaceId,
}: AddPaymentMethodFormProps) {
@@ -141,9 +139,7 @@ function AddPaymentMethodForm({
// payment method added successfylly
triggerToast(`New payment method added`);
close();
triggerToast('New payment method has been added to the workspace.');
discordAnnounce(
`(${user.email}) added a new credit card to workspace id: ${workspaceId}.`,
@@ -205,26 +201,27 @@ function AddPaymentMethodForm({
);
}
type BillingPaymentMethodFormProps = {
close: () => void;
onPaymentMethodAdded?: (e?: any) => Promise<void>;
export interface BillingPaymentMethodFormProps {
/**
* Callback function to run after a payment method is added.
*/
onPaymentMethodAdded?: (e?: any) => void;
/**
* Workspace identifier.
*/
workspaceId: string;
};
}
export function BillingPaymentMethodForm({
close,
export default function BillingPaymentMethodForm({
onPaymentMethodAdded,
workspaceId,
}: BillingPaymentMethodFormProps) {
return (
<Elements stripe={stripePromise}>
<AddPaymentMethodForm
close={close}
onPaymentMethodAdded={onPaymentMethodAdded}
workspaceId={workspaceId}
/>
</Elements>
);
}
export default BillingPaymentMethodForm;

View File

@@ -0,0 +1,2 @@
export * from './BillingPaymentMethodForm';
export { default as BillingPaymentMethodForm } from './BillingPaymentMethodForm';

View File

@@ -1,5 +1,5 @@
import { BillingPaymentMethodForm } from '@/components/billing-payment-method/BillingPaymentMethodForm';
import { useDialog } from '@/components/common/DialogProvider';
import { BillingPaymentMethodForm } from '@/components/workspace/BillingPaymentMethodForm';
import type { GetPaymentMethodsFragment } from '@/generated/graphql';
import {
refetchGetPaymentMethodsQuery,
@@ -8,7 +8,6 @@ import {
useSetNewDefaultPaymentMethodMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { Modal } from '@/ui/Modal';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Button from '@/ui/v2/Button';
import Table from '@/ui/v2/Table';
@@ -21,8 +20,6 @@ import Text from '@/ui/v2/Text';
import { triggerToast } from '@/utils/toast';
import { useTheme } from '@mui/material';
import { formatDistanceToNowStrict } from 'date-fns';
import { useRouter } from 'next/router';
import { useState } from 'react';
function CheckCircle() {
const theme = useTheme();
@@ -44,15 +41,8 @@ function CheckCircle() {
}
export default function WorkspacePaymentMethods() {
const router = useRouter();
const { action } = router.query;
const { currentWorkspace } = useCurrentWorkspaceAndProject();
const { openAlertDialog } = useDialog();
const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(
action === 'add-payment-method',
);
const { openAlertDialog, openDialog, closeDialog } = useDialog();
const { loading, error, data } = useGetPaymentMethodsQuery({
variables: {
@@ -230,7 +220,14 @@ export default function WorkspacePaymentMethods() {
<Button
variant="outlined"
onClick={() => {
setShowAddPaymentMethodModal(true);
openDialog({
component: (
<BillingPaymentMethodForm
workspaceId={currentWorkspace.id}
onPaymentMethodAdded={closeDialog}
/>
),
});
}}
disabled={maxPaymentMethodsReached}
>
@@ -244,19 +241,6 @@ export default function WorkspacePaymentMethods() {
payment methods.
</Text>
)}
{showAddPaymentMethodModal && (
<Modal
showModal={showAddPaymentMethodModal}
close={() =>
setShowAddPaymentMethodModal(!showAddPaymentMethodModal)
}
>
<BillingPaymentMethodForm
workspaceId={currentWorkspace.id}
close={() => setShowAddPaymentMethodModal(false)}
/>
</Modal>
)}
</div>
</div>
</div>

View File

@@ -1,9 +1,8 @@
import { useRouter } from 'next/router';
import type { PropsWithChildren } from 'react';
import { createContext, useContext, useMemo, useReducer } from 'react';
import { createContext, useContext, useMemo } from 'react';
export interface UIContextState {
paymentModal: boolean;
/**
* Determines whether or not the dashboard is in maintenance mode.
*/
@@ -12,42 +11,18 @@ export interface UIContextState {
* The date and time when maintenance mode will end.
*/
maintenanceEndDate: Date;
openPaymentModal: () => void;
closePaymentModal: () => void;
}
const initialState: UIContextState = {
paymentModal: false,
export const UIContext = createContext<UIContextState>({
maintenanceActive: false,
maintenanceEndDate: null,
openPaymentModal: () => {},
closePaymentModal: () => {},
};
export const UIContext = createContext<UIContextState>(initialState);
});
UIContext.displayName = 'UIContext';
function sideReducer(state: any, action: any) {
switch (action.type) {
case 'TOGGLE_PAYMENT_MODAL': {
return {
...state,
paymentModal: !state.paymentModal,
};
}
default:
return { ...state };
}
}
export function UIProvider(props: PropsWithChildren<unknown>) {
const [state, dispatch] = useReducer(sideReducer, initialState);
const router = useRouter();
const openPaymentModal = () => dispatch({ type: 'TOGGLE_PAYMENT_MODAL' });
const closePaymentModal = () => dispatch({ type: 'TOGGLE_PAYMENT_MODAL' });
const maintenanceUnlocked =
process.env.NEXT_PUBLIC_MAINTENANCE_UNLOCK_SECRET &&
process.env.NEXT_PUBLIC_MAINTENANCE_UNLOCK_SECRET ===
@@ -55,9 +30,6 @@ export function UIProvider(props: PropsWithChildren<unknown>) {
const value: UIContextState = useMemo(
() => ({
...state,
openPaymentModal,
closePaymentModal,
maintenanceActive: maintenanceUnlocked
? false
: process.env.NEXT_PUBLIC_MAINTENANCE_ACTIVE === 'true',
@@ -67,7 +39,7 @@ export function UIProvider(props: PropsWithChildren<unknown>) {
? new Date(Date.parse(process.env.NEXT_PUBLIC_MAINTENANCE_END_DATE))
: null,
}),
[state, maintenanceUnlocked],
[maintenanceUnlocked],
);
return <UIContext.Provider value={value} {...props} />;

View File

@@ -234,7 +234,6 @@ export default function SettingsGeneralPage() {
disabled: maintenanceActive,
onClick: () => {
openDialog({
title: '',
component: (
<RemoveApplicationModal
close={closeDialog}
@@ -243,7 +242,6 @@ export default function SettingsGeneralPage() {
),
props: {
PaperProps: { className: 'max-w-sm' },
hideTitle: true,
},
});
},

View File

@@ -1,11 +1,11 @@
import { BillingPaymentMethodForm } from '@/components/billing-payment-method/BillingPaymentMethodForm';
import { useDialog } from '@/components/common/DialogProvider';
import AuthenticatedLayout from '@/components/layout/AuthenticatedLayout';
import Container from '@/components/layout/Container';
import { BillingPaymentMethodForm } from '@/components/workspace/BillingPaymentMethodForm';
import { useUI } from '@/context/UIContext';
import features from '@/data/features.json';
import { useSubmitState } from '@/hooks/useSubmitState';
import { Alert } from '@/ui/Alert';
import { Modal } from '@/ui/Modal';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
@@ -44,7 +44,7 @@ import type { ApolloError } from '@apollo/client';
import { useUserData } from '@nhost/nextjs';
import Image from 'next/image';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import type { FormEvent, ReactElement } from 'react';
import { cloneElement, isValidElement, useState } from 'react';
import { toast } from 'react-hot-toast';
import slugify from 'slugify';
@@ -67,6 +67,7 @@ export function NewProjectPageContent({
preSelectedWorkspace,
preSelectedRegion,
}: NewAppPageProps) {
const { openDialog, closeDialog } = useDialog();
const { maintenanceActive } = useUI();
const router = useRouter();
@@ -102,11 +103,7 @@ export function NewProjectPageContent({
const [plan, setPlan] = useState(defaultSelectedPlan);
// state
const { submitState, setSubmitState } = useSubmitState();
const [showPaymentModal, setShowPaymentModal] = useState(false);
// graphql mutations
const [insertApp] = useInsertApplicationMutation({
refetchQueries: [GetAllWorkspacesAndProjectsDocument],
@@ -146,13 +143,8 @@ export function NewProjectPageContent({
setDatabasePassword(newRandomDatabasePassword);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!plan.isFree && workspace.paymentMethods.length === 0) {
setShowPaymentModal(true);
return;
}
async function handleCreateProject(event: FormEvent) {
event.preventDefault();
setSubmitState({
error: null,
@@ -225,7 +217,29 @@ export function NewProjectPageContent({
loading: false,
});
}
};
}
async function handleSubmit(event: FormEvent) {
event.preventDefault();
if (!plan.isFree && workspace.paymentMethods.length === 0) {
openDialog({
component: (
<BillingPaymentMethodForm
onPaymentMethodAdded={() => {
handleCreateProject(event);
closeDialog();
}}
workspaceId={workspace.id}
/>
),
});
return;
}
handleCreateProject(event);
}
if (!selectedWorkspace) {
return (
@@ -288,7 +302,13 @@ export function NewProjectPageContent({
const workspaceInList = workspaces.find(
({ id }) => id === value,
);
setPlan(plans[0]);
if (numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS) {
setPlan(plans.find((currentPlan) => !currentPlan.isFree));
} else {
setPlan(plans[0]);
}
setSelectedWorkspace({
id: workspaceInList.id,
name: workspaceInList.name,
@@ -561,23 +581,6 @@ export function NewProjectPageContent({
)}
<div className="flex justify-end">
{showPaymentModal && (
<Modal
showModal={showPaymentModal}
close={() => {
setShowPaymentModal(false);
}}
>
<BillingPaymentMethodForm
close={() => {
setShowPaymentModal(false);
}}
onPaymentMethodAdded={handleSubmit}
workspaceId={workspace.id}
/>
</Modal>
)}
<Button
type="submit"
loading={submitState.loading}
@@ -597,7 +600,9 @@ export default function NewProjectPage() {
const router = useRouter();
const user = useUserData();
const { data, loading, error } = usePrefetchNewAppQuery();
const { data, loading, error } = usePrefetchNewAppQuery({
fetchPolicy: 'cache-and-network',
});
const {
data: freeAndActiveProjectsData,