mirror of
				https://github.com/binwiederhier/ntfy.git
				synced 2025-10-31 21:12:36 +01:00 
			
		
		
		
	publishSyncEvent, Stripe endpoint changes
This commit is contained in:
		
							parent
							
								
									7faed3ee1e
								
							
						
					
					
						commit
						83de879894
					
				
					 14 changed files with 424 additions and 262 deletions
				
			
		|  | @ -179,8 +179,10 @@ | |||
|   "account_usage_unlimited": "Unlimited", | ||||
|   "account_usage_limits_reset_daily": "Usage limits are reset daily at midnight (UTC)", | ||||
|   "account_usage_tier_title": "Account type", | ||||
|   "account_usage_tier_description": "Your account's power level", | ||||
|   "account_usage_tier_admin": "Admin", | ||||
|   "account_usage_tier_none": "Basic", | ||||
|   "account_usage_tier_basic": "Basic", | ||||
|   "account_usage_tier_free": "Free", | ||||
|   "account_usage_tier_upgrade_button": "Upgrade to Pro", | ||||
|   "account_usage_tier_change_button": "Change", | ||||
|   "account_usage_tier_paid_until": "Subscription paid until {{date}}, and will auto-renew", | ||||
|  | @ -199,6 +201,8 @@ | |||
|   "account_delete_dialog_label": "Type '{{username}}' to delete account", | ||||
|   "account_delete_dialog_button_cancel": "Cancel", | ||||
|   "account_delete_dialog_button_submit": "Permanently delete account", | ||||
|   "account_upgrade_dialog_title": "Change billing plan", | ||||
|   "account_upgrade_dialog_cancel_warning": "This will cancel your subscription, and downgrade your account on {{date}}. On that date, topic reservations as well as messages cached on the server will be deleted.", | ||||
|   "prefs_notifications_title": "Notifications", | ||||
|   "prefs_notifications_sound_title": "Notification sound", | ||||
|   "prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive", | ||||
|  |  | |||
|  | @ -264,11 +264,20 @@ class AccountApi { | |||
|         this.triggerChange(); // Dangle!
 | ||||
|     } | ||||
| 
 | ||||
|     async createBillingSubscription(tier) { | ||||
|         console.log(`[AccountApi] Creating billing subscription with ${tier}`); | ||||
|         return await this.upsertBillingSubscription("POST", tier) | ||||
|     } | ||||
| 
 | ||||
|     async updateBillingSubscription(tier) { | ||||
|         console.log(`[AccountApi] Updating billing subscription with ${tier}`); | ||||
|         return await this.upsertBillingSubscription("PUT", tier) | ||||
|     } | ||||
| 
 | ||||
|     async upsertBillingSubscription(method, tier) { | ||||
|         const url = accountBillingSubscriptionUrl(config.base_url); | ||||
|         console.log(`[AccountApi] Requesting tier change to ${tier}`); | ||||
|         const response = await fetch(url, { | ||||
|             method: "POST", | ||||
|             method: method, | ||||
|             headers: withBearerAuth({}, session.token()), | ||||
|             body: JSON.stringify({ | ||||
|                 tier: tier | ||||
|  | @ -284,7 +293,7 @@ class AccountApi { | |||
| 
 | ||||
|     async deleteBillingSubscription() { | ||||
|         const url = accountBillingSubscriptionUrl(config.base_url); | ||||
|         console.log(`[AccountApi] Cancelling paid subscription`); | ||||
|         console.log(`[AccountApi] Cancelling billing subscription`); | ||||
|         const response = await fetch(url, { | ||||
|             method: "DELETE", | ||||
|             headers: withBearerAuth({}, session.token()) | ||||
|  | @ -345,6 +354,7 @@ class AccountApi { | |||
|     } | ||||
| 
 | ||||
|     async triggerChange() { | ||||
|         return null; | ||||
|         const account = await this.get(); | ||||
|         if (!account.sync_topic) { | ||||
|             return; | ||||
|  |  | |||
|  | @ -56,6 +56,7 @@ const Basics = () => { | |||
|             <PrefGroup> | ||||
|                 <Username/> | ||||
|                 <ChangePassword/> | ||||
|                 <AccountType/> | ||||
|             </PrefGroup> | ||||
|         </Card> | ||||
|     ); | ||||
|  | @ -168,18 +169,20 @@ const ChangePasswordDialog = (props) => { | |||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const Stats = () => { | ||||
| const AccountType = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     const { account } = useContext(AccountContext); | ||||
|     const [upgradeDialogKey, setUpgradeDialogKey] = useState(0); | ||||
|     const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); | ||||
| 
 | ||||
|     if (!account) { | ||||
|         return <></>; | ||||
|     } | ||||
| 
 | ||||
|     const normalize = (value, max) => { | ||||
|         return Math.min(value / max * 100, 100); | ||||
|     }; | ||||
|     const handleUpgradeClick = () => { | ||||
|         setUpgradeDialogKey(k => k + 1); | ||||
|         setUpgradeDialogOpen(true); | ||||
|     } | ||||
| 
 | ||||
|     const handleManageBilling = async () => { | ||||
|         try { | ||||
|  | @ -194,67 +197,89 @@ const Stats = () => { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     let accountType; | ||||
|     if (account.role === "admin") { | ||||
|         const tierSuffix = (account.tier) ? `(with ${account.tier.name} tier)` : `(no tier)`; | ||||
|         accountType = `${t("account_usage_tier_admin")} ${tierSuffix}`; | ||||
|     } else if (!account.tier) { | ||||
|         accountType = (config.enable_payments) ? t("account_usage_tier_free") : t("account_usage_tier_basic"); | ||||
|     } else { | ||||
|         accountType = account.tier.name; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <Pref | ||||
|             alignTop={account.billing?.status === "past_due" || account.billing?.cancel_at > 0} | ||||
|             title={t("account_usage_tier_title")} | ||||
|             description={t("account_usage_tier_description")} | ||||
|         > | ||||
|             <div> | ||||
|                 {accountType} | ||||
|                 {account.billing?.paid_until && !account.billing?.cancel_at && | ||||
|                     <Tooltip title={t("account_usage_tier_paid_until", { date: formatShortDate(account.billing?.paid_until) })}> | ||||
|                         <span><InfoIcon/></span> | ||||
|                     </Tooltip> | ||||
|                 } | ||||
|                 {config.enable_payments && account.role === "user" && !account.billing?.subscription && | ||||
|                     <Button | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                         startIcon={<CelebrationIcon sx={{ color: "#55b86e" }}/>} | ||||
|                         onClick={handleUpgradeClick} | ||||
|                         sx={{ml: 1}} | ||||
|                     >{t("account_usage_tier_upgrade_button")}</Button> | ||||
|                 } | ||||
|                 {config.enable_payments && account.role === "user" && account.billing?.subscription && | ||||
|                     <Button | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                         onClick={handleUpgradeClick} | ||||
|                         sx={{ml: 1}} | ||||
|                     >{t("account_usage_tier_change_button")}</Button> | ||||
|                 } | ||||
|                 {config.enable_payments && account.role === "user" && account.billing?.customer && | ||||
|                     <Button | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                         onClick={handleManageBilling} | ||||
|                         sx={{ml: 1}} | ||||
|                     >{t("account_usage_manage_billing_button")}</Button> | ||||
|                 } | ||||
|                 <UpgradeDialog | ||||
|                     key={`upgradeDialogFromAccount${upgradeDialogKey}`} | ||||
|                     open={upgradeDialogOpen} | ||||
|                     onCancel={() => setUpgradeDialogOpen(false)} | ||||
|                 /> | ||||
|             </div> | ||||
|             {account.billing?.status === "past_due" && | ||||
|                 <Alert severity="error" sx={{mt: 1}}>{t("account_usage_tier_payment_overdue")}</Alert> | ||||
|             } | ||||
|             {account.billing?.cancel_at > 0 && | ||||
|                 <Alert severity="warning" sx={{mt: 1}}>{t("account_usage_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })}</Alert> | ||||
|             } | ||||
|         </Pref> | ||||
|     ) | ||||
| }; | ||||
| 
 | ||||
| const Stats = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     const { account } = useContext(AccountContext); | ||||
|     const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); | ||||
| 
 | ||||
|     if (!account) { | ||||
|         return <></>; | ||||
|     } | ||||
| 
 | ||||
|     const normalize = (value, max) => { | ||||
|         return Math.min(value / max * 100, 100); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <Card sx={{p: 3}} aria-label={t("account_usage_title")}> | ||||
|             <Typography variant="h5" sx={{marginBottom: 2}}> | ||||
|                 {t("account_usage_title")} | ||||
|             </Typography> | ||||
|             <PrefGroup> | ||||
|                 <Pref | ||||
|                     alignTop={account.billing?.status === "past_due" || account.billing?.cancel_at > 0} | ||||
|                     title={t("account_usage_tier_title")} | ||||
|                 > | ||||
|                     <div> | ||||
|                         {account.role === "admin" && | ||||
|                             <> | ||||
|                                 {t("account_usage_tier_admin")} | ||||
|                                 {" "}{account.tier ? `(with ${account.tier.name} tier)` : `(no tier)`} | ||||
|                             </> | ||||
|                         } | ||||
|                         {account.role === "user" && account.tier && account.tier.name} | ||||
|                         {account.role === "user" && !account.tier && t("account_usage_tier_none")} | ||||
|                         {account.billing?.paid_until && | ||||
|                             <Tooltip title={t("account_usage_tier_paid_until", { date: formatShortDate(account.billing?.paid_until) })}> | ||||
|                                 <span><InfoIcon/></span> | ||||
|                             </Tooltip> | ||||
|                         } | ||||
|                         {config.enable_payments && account.role === "user" && (!account.tier || !account.tier.paid) && | ||||
|                             <Button | ||||
|                                 variant="outlined" | ||||
|                                 size="small" | ||||
|                                 startIcon={<CelebrationIcon sx={{ color: "#55b86e" }}/>} | ||||
|                                 onClick={() => setUpgradeDialogOpen(true)} | ||||
|                                 sx={{ml: 1}} | ||||
|                             >{t("account_usage_tier_upgrade_button")}</Button> | ||||
|                         } | ||||
|                         {config.enable_payments && account.role === "user" && account.tier?.paid && | ||||
|                             <Button | ||||
|                                 variant="outlined" | ||||
|                                 size="small" | ||||
|                                 onClick={() => setUpgradeDialogOpen(true)} | ||||
|                                 sx={{ml: 1}} | ||||
|                             >{t("account_usage_tier_change_button")}</Button> | ||||
|                         } | ||||
|                         {config.enable_payments && account.role === "user" && account.billing?.customer && | ||||
|                             <Button | ||||
|                                 variant="outlined" | ||||
|                                 size="small" | ||||
|                                 onClick={handleManageBilling} | ||||
|                                 sx={{ml: 1}} | ||||
|                             >{t("account_usage_manage_billing_button")}</Button> | ||||
|                         } | ||||
|                         <UpgradeDialog | ||||
|                             open={upgradeDialogOpen} | ||||
|                             onCancel={() => setUpgradeDialogOpen(false)} | ||||
|                         /> | ||||
|                     </div> | ||||
|                     {account.billing?.status === "past_due" && | ||||
|                         <Alert severity="error" sx={{mt: 1}}>{t("account_usage_tier_payment_overdue")}</Alert> | ||||
|                     } | ||||
|                     {account.billing?.cancel_at > 0 && | ||||
|                         <Alert severity="info" sx={{mt: 1}}>{t("account_usage_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })}</Alert> | ||||
|                     } | ||||
|                 </Pref> | ||||
|                 {account.role !== "admin" && | ||||
|                     <Pref title={t("account_usage_reservations_title")}> | ||||
|                         {account.limits.reservations > 0 && | ||||
|  |  | |||
|  | @ -103,8 +103,8 @@ const NavList = (props) => { | |||
|     }; | ||||
| 
 | ||||
|     const isAdmin = account?.role === "admin"; | ||||
|     const isPaid = account?.tier?.paid; | ||||
|     const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid;// && (!props.account || !props.account.tier || !props.account.tier.paid || props.account);
 | ||||
|     const isPaid = account?.billing?.subscription; | ||||
|     const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid; | ||||
|     const showSubscriptionsList = props.subscriptions?.length > 0; | ||||
|     const showNotificationBrowserNotSupportedBox = !notifier.browserSupported(); | ||||
|     const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
 | ||||
|  | @ -174,7 +174,14 @@ const NavList = (props) => { | |||
| }; | ||||
| 
 | ||||
| const UpgradeBanner = () => { | ||||
|     const [dialogKey, setDialogKey] = useState(0); | ||||
|     const [dialogOpen, setDialogOpen] = useState(false); | ||||
| 
 | ||||
|     const handleClick = () => { | ||||
|         setDialogKey(k => k + 1); | ||||
|         setDialogOpen(true); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ | ||||
|             position: "fixed", | ||||
|  | @ -184,7 +191,7 @@ const UpgradeBanner = () => { | |||
|             background: "linear-gradient(150deg, rgba(196, 228, 221, 0.46) 0%, rgb(255, 255, 255) 100%)", | ||||
|         }}> | ||||
|             <Divider/> | ||||
|             <ListItemButton onClick={() => setDialogOpen(true)} sx={{pt: 2, pb: 2}}> | ||||
|             <ListItemButton onClick={handleClick} sx={{pt: 2, pb: 2}}> | ||||
|                 <ListItemIcon><CelebrationIcon sx={{ color: "#55b86e" }} fontSize="large"/></ListItemIcon> | ||||
|                 <ListItemText | ||||
|                     sx={{ ml: 1 }} | ||||
|  | @ -207,6 +214,7 @@ const UpgradeBanner = () => { | |||
|                 /> | ||||
|             </ListItemButton> | ||||
|             <UpgradeDialog | ||||
|                 key={`upgradeDialog${dialogKey}`} | ||||
|                 open={dialogOpen} | ||||
|                 onCancel={() => setDialogOpen(false)} | ||||
|             /> | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import * as React from 'react'; | |||
| import Dialog from '@mui/material/Dialog'; | ||||
| import DialogContent from '@mui/material/DialogContent'; | ||||
| import DialogTitle from '@mui/material/DialogTitle'; | ||||
| import {CardActionArea, CardContent, useMediaQuery} from "@mui/material"; | ||||
| import {Alert, CardActionArea, CardContent, useMediaQuery} from "@mui/material"; | ||||
| import theme from "./theme"; | ||||
| import DialogFooter from "./DialogFooter"; | ||||
| import Button from "@mui/material/Button"; | ||||
|  | @ -13,28 +13,53 @@ import {useContext, useState} from "react"; | |||
| import Card from "@mui/material/Card"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import {AccountContext} from "./App"; | ||||
| import {formatShortDate} from "../app/utils"; | ||||
| import {useTranslation} from "react-i18next"; | ||||
| 
 | ||||
| const UpgradeDialog = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const { account } = useContext(AccountContext); | ||||
|     const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||
|     const [newTier, setNewTier] = useState(account?.tier?.code || null); | ||||
|     const [errorText, setErrorText] = useState(""); | ||||
| 
 | ||||
|     const handleCheckout = async () => { | ||||
|         try { | ||||
|             if (newTier == null) { | ||||
|                 await accountApi.deleteBillingSubscription(); | ||||
|             } else { | ||||
|                 const response = await accountApi.updateBillingSubscription(newTier); | ||||
|                 if (response.redirect_url) { | ||||
|                     window.location.href = response.redirect_url; | ||||
|                 } else { | ||||
|                     await accountApi.sync(); | ||||
|                 } | ||||
|             } | ||||
|     if (!account) { | ||||
|         return <></>; | ||||
|     } | ||||
| 
 | ||||
|     const currentTier = account.tier?.code || null; | ||||
|     let action, submitButtonLabel, submitButtonEnabled; | ||||
|     if (currentTier === newTier) { | ||||
|         submitButtonLabel = "Update subscription"; | ||||
|         submitButtonEnabled = false; | ||||
|         action = null; | ||||
|     } else if (currentTier === null) { | ||||
|         submitButtonLabel = "Pay $5 now and subscribe"; | ||||
|         submitButtonEnabled = true; | ||||
|         action = Action.CREATE; | ||||
|     } else if (newTier === null) { | ||||
|         submitButtonLabel = "Cancel subscription"; | ||||
|         submitButtonEnabled = true; | ||||
|         action = Action.CANCEL; | ||||
|     } else { | ||||
|         submitButtonLabel = "Update subscription"; | ||||
|         submitButtonEnabled = true; | ||||
|         action = Action.UPDATE; | ||||
|     } | ||||
| 
 | ||||
|     const handleSubmit = async () => { | ||||
|         try { | ||||
|             if (action === Action.CREATE) { | ||||
|                 const response = await accountApi.createBillingSubscription(newTier); | ||||
|                 window.location.href = response.redirect_url; | ||||
|             } else if (action === Action.UPDATE) { | ||||
|                 await accountApi.updateBillingSubscription(newTier); | ||||
|             } else if (action === Action.CANCEL) { | ||||
|                 await accountApi.deleteBillingSubscription(); | ||||
|             } | ||||
|             props.onCancel(); | ||||
|         } catch (e) { | ||||
|             console.log(`[UpgradeDialog] Error creating checkout session`, e); | ||||
|             console.log(`[UpgradeDialog] Error changing billing subscription`, e); | ||||
|             if ((e instanceof UnauthorizedError)) { | ||||
|                 session.resetAndRedirect(routes.login); | ||||
|             } | ||||
|  | @ -44,7 +69,7 @@ const UpgradeDialog = (props) => { | |||
| 
 | ||||
|     return ( | ||||
|         <Dialog open={props.open} onClose={props.onCancel} maxWidth="md" fullScreen={fullScreen}> | ||||
|             <DialogTitle>Upgrade to Pro</DialogTitle> | ||||
|             <DialogTitle>Change billing plan</DialogTitle> | ||||
|             <DialogContent> | ||||
|                 <div style={{ | ||||
|                     display: "flex", | ||||
|  | @ -55,9 +80,15 @@ const UpgradeDialog = (props) => { | |||
|                     <TierCard code="pro" name={"Pro"} selected={newTier === "pro"} onClick={() => setNewTier("pro")}/> | ||||
|                     <TierCard code="business" name={"Business"} selected={newTier === "business"} onClick={() => setNewTier("business")}/> | ||||
|                 </div> | ||||
|                 {action === Action.CANCEL && | ||||
|                     <Alert severity="warning"> | ||||
|                         {t("account_upgrade_dialog_cancel_warning", { date: formatShortDate(account.billing.paid_until) })} | ||||
|                     </Alert> | ||||
|                 } | ||||
|             </DialogContent> | ||||
|             <DialogFooter status={errorText}> | ||||
|                 <Button onClick={handleCheckout}>Checkout</Button> | ||||
|                 <Button onClick={props.onCancel}>Cancel</Button> | ||||
|                 <Button onClick={handleSubmit} disabled={!submitButtonEnabled}>{submitButtonLabel}</Button> | ||||
|             </DialogFooter> | ||||
|         </Dialog> | ||||
|     ); | ||||
|  | @ -65,8 +96,7 @@ const UpgradeDialog = (props) => { | |||
| 
 | ||||
| const TierCard = (props) => { | ||||
|     const cardStyle = (props.selected) ? { | ||||
|         border: "1px solid red", | ||||
| 
 | ||||
|         background: "#eee" | ||||
|     } : {}; | ||||
|     return ( | ||||
|         <Card sx={{ m: 1, maxWidth: 345 }}> | ||||
|  | @ -85,4 +115,10 @@ const TierCard = (props) => { | |||
|     ); | ||||
| } | ||||
| 
 | ||||
| const Action = { | ||||
|     CREATE: 1, | ||||
|     UPDATE: 2, | ||||
|     CANCEL: 3 | ||||
| }; | ||||
| 
 | ||||
| export default UpgradeDialog; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 binwiederhier
						binwiederhier