diff --git a/actions/cancelSubscription.ts b/actions/cancelSubscription.ts deleted file mode 100644 index fb3aae9c..00000000 --- a/actions/cancelSubscription.ts +++ /dev/null @@ -1,40 +0,0 @@ -"use server"; - -import { getIdentityData } from "./getIdentityData"; -import { getStripe } from "stripe/client"; -import { supabaseServerClient } from "supabase/serverClient"; -import { Ok, Err, type Result } from "src/result"; - -export async function cancelSubscription(): Promise< - Result<{ cancelAt: string }, string> -> { - const identity = await getIdentityData(); - if (!identity) { - return Err("Not authenticated"); - } - - const { data: sub } = await supabaseServerClient - .from("user_subscriptions") - .select("stripe_subscription_id, current_period_end") - .eq("identity_id", identity.id) - .single(); - - if (!sub?.stripe_subscription_id) { - return Err("No active subscription found"); - } - - await getStripe().subscriptions.update(sub.stripe_subscription_id, { - cancel_at_period_end: true, - }); - - // Optimistic update - await supabaseServerClient - .from("user_subscriptions") - .update({ - status: "canceling", - updated_at: new Date().toISOString(), - }) - .eq("identity_id", identity.id); - - return Ok({ cancelAt: sub.current_period_end || "" }); -} diff --git a/actions/createBillingPortalSession.ts b/actions/createBillingPortalSession.ts new file mode 100644 index 00000000..7dea85a1 --- /dev/null +++ b/actions/createBillingPortalSession.ts @@ -0,0 +1,32 @@ +"use server"; + +import { getIdentityData } from "./getIdentityData"; +import { getStripe } from "stripe/client"; +import { supabaseServerClient } from "supabase/serverClient"; +import { Ok, Err, type Result } from "src/result"; + +export async function createBillingPortalSession( + returnUrl: string, +): Promise> { + const identity = await getIdentityData(); + if (!identity) { + return Err("Not authenticated"); + } + + const { data: sub } = await supabaseServerClient + .from("user_subscriptions") + .select("stripe_customer_id") + .eq("identity_id", identity.id) + .single(); + + if (!sub?.stripe_customer_id) { + return Err("No subscription found"); + } + + const session = await getStripe().billingPortal.sessions.create({ + customer: sub.stripe_customer_id, + return_url: returnUrl, + }); + + return Ok({ url: session.url }); +} diff --git a/actions/reactivateSubscription.ts b/actions/reactivateSubscription.ts deleted file mode 100644 index fab67d04..00000000 --- a/actions/reactivateSubscription.ts +++ /dev/null @@ -1,40 +0,0 @@ -"use server"; - -import { getIdentityData } from "./getIdentityData"; -import { getStripe } from "stripe/client"; -import { supabaseServerClient } from "supabase/serverClient"; -import { Ok, Err, type Result } from "src/result"; - -export async function reactivateSubscription(): Promise< - Result<{ renewsAt: string }, string> -> { - const identity = await getIdentityData(); - if (!identity) { - return Err("Not authenticated"); - } - - const { data: sub } = await supabaseServerClient - .from("user_subscriptions") - .select("stripe_subscription_id, current_period_end") - .eq("identity_id", identity.id) - .single(); - - if (!sub?.stripe_subscription_id) { - return Err("No active subscription found"); - } - - await getStripe().subscriptions.update(sub.stripe_subscription_id, { - cancel_at_period_end: false, - }); - - // Optimistic update - await supabaseServerClient - .from("user_subscriptions") - .update({ - status: "active", - updated_at: new Date().toISOString(), - }) - .eq("identity_id", identity.id); - - return Ok({ renewsAt: sub.current_period_end || "" }); -} diff --git a/app/(home-pages)/home/Actions/AccountSettings.tsx b/app/(home-pages)/home/Actions/AccountSettings.tsx index dcab1a7d..44e7be54 100644 --- a/app/(home-pages)/home/Actions/AccountSettings.tsx +++ b/app/(home-pages)/home/Actions/AccountSettings.tsx @@ -1,24 +1,12 @@ "use client"; -import { useState } from "react"; -import { mutate } from "swr"; import { ActionButton } from "components/ActionBar/ActionButton"; import { Popover } from "components/Popover"; import { ThemeSetterContent } from "components/ThemeManager/ThemeSetter"; import { useIsMobile } from "src/hooks/isMobile"; import { PaintSmall } from "components/Icons/PaintSmall"; -import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; -import { GoBackSmall } from "components/Icons/GoBackSmall"; -import { LogoutSmall } from "components/Icons/LogoutSmall"; -import { ManageProSubscription } from "app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription"; -import { Modal } from "components/Modal"; -import { UpgradeContent } from "app/lish/[did]/[publication]/UpgradeModal"; -import { useIsPro } from "src/hooks/useEntitlement"; export const AccountSettings = (props: { entityID: string }) => { - let [state, setState] = useState< - "menu" | "general" | "theme" | "manage-subscription" - >("menu"); let isMobile = useIsMobile(); return ( @@ -26,140 +14,11 @@ export const AccountSettings = (props: { entityID: string }) => { asChild side={isMobile ? "top" : "right"} align={isMobile ? "center" : "start"} - className={`w-xs bg-white!`} + className={`w-xs bg-white!`} arrowFill="bg-white" trigger={ label="Theme" />} > - {state === "general" ? ( - setState("menu")} /> - ) : state === "theme" ? ( - setState("menu")} - /> - ) : state === "manage-subscription" ? ( - setState("menu")} /> - ) : ( - - )} - - ); -}; - -const SettingsMenu = (props: { - state: "menu" | "general" | "theme" | "manage-subscription"; - setState: (s: typeof props.state) => void; -}) => { - let menuItemClassName = - "menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!"; - - let isPro = useIsPro(); - - return ( -
- - - - {!isPro ? ( - - Get Leaflet Pro - {" "} -
- } - > - - - ) : ( - - )} - - ); -}; - -const GeneralSettings = (props: { backToMenu: () => void }) => { - return ( -
- props.backToMenu()} - /> - - -
- ); -}; -const AccountThemeSettings = (props: { - entityID: string; - backToMenu: () => void; -}) => { - return ( -
- props.backToMenu()} - /> -
- ); -}; -export const AccountSettingsHeader = (props: { - state: "menu" | "general" | "theme"; - backToMenuAction?: () => void; -}) => { - return ( -
- {props.state === "menu" - ? "Settings" - : props.state === "general" - ? "General" - : props.state === "theme" - ? "Account Theme" - : ""} - {props.backToMenuAction && ( - - )} -
+ ); }; diff --git a/app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx b/app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx index 2723baa2..f878dcd9 100644 --- a/app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx +++ b/app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx @@ -1,19 +1,15 @@ import { useState } from "react"; import { ButtonPrimary } from "components/Buttons"; -import { PubSettingsHeader } from "./PublicationSettings"; -import { cancelSubscription } from "actions/cancelSubscription"; -import { reactivateSubscription } from "actions/reactivateSubscription"; +import { createBillingPortalSession } from "actions/createBillingPortalSession"; import { useIdentityData } from "components/IdentityProvider"; import { DotLoader } from "components/utils/DotLoader"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; +import { GoBackSmall } from "components/Icons/GoBackSmall"; export const ManageProSubscription = (props: { backToMenu: () => void }) => { - const [state, setState] = useState<"manage" | "confirm" | "success">( - "manage", - ); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const { identity, mutate } = useIdentityData(); + const { identity } = useIdentityData(); const subscription = identity?.subscription; const renewalDate = useLocalizedDate( @@ -21,94 +17,48 @@ export const ManageProSubscription = (props: { backToMenu: () => void }) => { { month: "long", day: "numeric", year: "numeric" }, ); - async function handleCancel() { - setLoading(true); - setError(null); - let result = await cancelSubscription(); - if (result.ok) { - setState("success"); - mutate(); - } else { - setError(result.error); - } - setLoading(false); - } - - async function handleReactivate() { + async function handleManageBilling() { setLoading(true); setError(null); - let result = await reactivateSubscription(); + const result = await createBillingPortalSession(window.location.href); if (result.ok) { - mutate(); + window.location.href = result.value.url; } else { setError(result.error); + setLoading(false); } - setLoading(false); } return (
- +
Manage Subscription - + +
- {state === "manage" && ( - <> -
- You have a
- {subscription?.plan || "Pro"} subscription -
- {subscription?.plan || "Leaflet Pro"} -
- {subscription?.status === "canceled" - ? "Your subscription has ended" - : subscription?.status === "canceling" - ? `Access until ${renewalDate}` - : `Renews on ${renewalDate}`} -
- {subscription?.status === "canceling" && ( - - {loading ? : "Reactivate Subscription"} - - )} - {error &&
{error}
} - {subscription?.status !== "canceling" && - subscription?.status !== "canceled" && ( - setState("confirm")} - > - Cancel Subscription - - )} - - )} - {state === "confirm" && ( - <> -
Are you sure you'd like to cancel your subscription?
- - {loading ? : "Yes, Cancel it"} - - {error &&
{error}
} - - )} - {state === "success" && ( -
- Your subscription has been cancelled. You'll have access until{" "} - {renewalDate}. +
+ You have a
+ {subscription?.plan || "Pro"} subscription +
+ {subscription?.plan || "Leaflet Pro"}
- )} + {subscription?.status === "canceled" + ? "Your subscription has ended" + : subscription?.status === "canceling" + ? `Access until ${renewalDate}` + : `Renews on ${renewalDate}`} +
+ + {loading ? : "Manage Billing"} + + {error &&
{error}
}
); diff --git a/app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx b/app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx index 4466ad6d..cd5545eb 100644 --- a/app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx +++ b/app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx @@ -13,17 +13,8 @@ import { ButtonPrimary } from "components/Buttons"; import { DotLoader } from "components/utils/DotLoader"; import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; import { PostOptions } from "./PostOptions"; -import { UpgradeContent } from "../../UpgradeModal"; -import { Modal } from "components/Modal"; -import { ManageProSubscription } from "./ManageProSubscription"; -import { useIsPro } from "src/hooks/useEntitlement"; -type menuState = - | "menu" - | "pub-settings" - | "theme" - | "post-settings" - | "manage-subscription"; +type menuState = "menu" | "pub-settings" | "theme" | "post-settings"; export function PublicationSettingsButton(props: { publication: string }) { let isMobile = useIsMobile(); @@ -65,8 +56,6 @@ export function PublicationSettingsButton(props: { publication: string }) { loading={loading} setLoading={setLoading} /> - ) : state === "manage-subscription" ? ( - setState("menu")} /> ) : ( { let menuItemClassName = "menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!"; - let isPro = useIsPro(); return (
@@ -119,28 +107,6 @@ const PubSettingsMenu = (props: { Theme and Layout - {!isPro ? ( - - Get Leaflet Pro - {" "} -
- } - > - - - ) : ( - - )}
); }; diff --git a/components/ActionBar/ProfileButton.tsx b/components/ActionBar/ProfileButton.tsx index 218101c2..a6da2387 100644 --- a/components/ActionBar/ProfileButton.tsx +++ b/components/ActionBar/ProfileButton.tsx @@ -3,22 +3,32 @@ import { ActionButton } from "./ActionButton"; import { useIdentityData } from "components/IdentityProvider"; import { AccountSmall } from "components/Icons/AccountSmall"; import { useRecordFromDid } from "src/utils/useRecordFromDid"; -import { Menu, MenuItem } from "components/Menu"; import { useIsMobile } from "src/hooks/isMobile"; import { LogoutSmall } from "components/Icons/LogoutSmall"; import { mutate } from "swr"; import { SpeedyLink } from "components/SpeedyLink"; +import { Popover } from "components/Popover"; +import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; +import { Modal } from "components/Modal"; +import { UpgradeContent } from "app/lish/[did]/[publication]/UpgradeModal"; +import { ManageProSubscription } from "app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription"; +import { useIsPro } from "src/hooks/useEntitlement"; +import { useState } from "react"; export const ProfileButton = () => { let { identity } = useIdentityData(); let { data: record } = useRecordFromDid(identity?.atp_did); let isMobile = useIsMobile(); + let [state, setState] = useState<"menu" | "manage-subscription">("menu"); + let isPro = useIsPro(); return ( - setState("menu")} + className="w-xs" trigger={ { /> } > - {record && ( - <> - - {}}>View Profile - + {state === "manage-subscription" ? ( + setState("menu")} /> + ) : ( +
+ {record && ( + <> + + + + +
+ + )} + {!isPro ? ( + + Get Leaflet Pro + +
+ } + > + + + ) : ( + + )}
- + + + )} - { - await fetch("/api/auth/logout"); - mutate("identity", null); - }} - > - - Log Out - -
+ ); };