tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
use stripe billing portal
awarm.space
3 days ago
914b770c
8119e5a4
+135
-358
7 changed files
expand all
collapse all
unified
split
actions
cancelSubscription.ts
createBillingPortalSession.ts
reactivateSubscription.ts
app
(home-pages)
home
Actions
AccountSettings.tsx
lish
[did]
[publication]
dashboard
settings
ManageProSubscription.tsx
PublicationSettings.tsx
components
ActionBar
ProfileButton.tsx
-40
actions/cancelSubscription.ts
···
1
1
-
"use server";
2
2
-
3
3
-
import { getIdentityData } from "./getIdentityData";
4
4
-
import { getStripe } from "stripe/client";
5
5
-
import { supabaseServerClient } from "supabase/serverClient";
6
6
-
import { Ok, Err, type Result } from "src/result";
7
7
-
8
8
-
export async function cancelSubscription(): Promise<
9
9
-
Result<{ cancelAt: string }, string>
10
10
-
> {
11
11
-
const identity = await getIdentityData();
12
12
-
if (!identity) {
13
13
-
return Err("Not authenticated");
14
14
-
}
15
15
-
16
16
-
const { data: sub } = await supabaseServerClient
17
17
-
.from("user_subscriptions")
18
18
-
.select("stripe_subscription_id, current_period_end")
19
19
-
.eq("identity_id", identity.id)
20
20
-
.single();
21
21
-
22
22
-
if (!sub?.stripe_subscription_id) {
23
23
-
return Err("No active subscription found");
24
24
-
}
25
25
-
26
26
-
await getStripe().subscriptions.update(sub.stripe_subscription_id, {
27
27
-
cancel_at_period_end: true,
28
28
-
});
29
29
-
30
30
-
// Optimistic update
31
31
-
await supabaseServerClient
32
32
-
.from("user_subscriptions")
33
33
-
.update({
34
34
-
status: "canceling",
35
35
-
updated_at: new Date().toISOString(),
36
36
-
})
37
37
-
.eq("identity_id", identity.id);
38
38
-
39
39
-
return Ok({ cancelAt: sub.current_period_end || "" });
40
40
-
}
+32
actions/createBillingPortalSession.ts
···
1
1
+
"use server";
2
2
+
3
3
+
import { getIdentityData } from "./getIdentityData";
4
4
+
import { getStripe } from "stripe/client";
5
5
+
import { supabaseServerClient } from "supabase/serverClient";
6
6
+
import { Ok, Err, type Result } from "src/result";
7
7
+
8
8
+
export async function createBillingPortalSession(
9
9
+
returnUrl: string,
10
10
+
): Promise<Result<{ url: string }, string>> {
11
11
+
const identity = await getIdentityData();
12
12
+
if (!identity) {
13
13
+
return Err("Not authenticated");
14
14
+
}
15
15
+
16
16
+
const { data: sub } = await supabaseServerClient
17
17
+
.from("user_subscriptions")
18
18
+
.select("stripe_customer_id")
19
19
+
.eq("identity_id", identity.id)
20
20
+
.single();
21
21
+
22
22
+
if (!sub?.stripe_customer_id) {
23
23
+
return Err("No subscription found");
24
24
+
}
25
25
+
26
26
+
const session = await getStripe().billingPortal.sessions.create({
27
27
+
customer: sub.stripe_customer_id,
28
28
+
return_url: returnUrl,
29
29
+
});
30
30
+
31
31
+
return Ok({ url: session.url });
32
32
+
}
-40
actions/reactivateSubscription.ts
···
1
1
-
"use server";
2
2
-
3
3
-
import { getIdentityData } from "./getIdentityData";
4
4
-
import { getStripe } from "stripe/client";
5
5
-
import { supabaseServerClient } from "supabase/serverClient";
6
6
-
import { Ok, Err, type Result } from "src/result";
7
7
-
8
8
-
export async function reactivateSubscription(): Promise<
9
9
-
Result<{ renewsAt: string }, string>
10
10
-
> {
11
11
-
const identity = await getIdentityData();
12
12
-
if (!identity) {
13
13
-
return Err("Not authenticated");
14
14
-
}
15
15
-
16
16
-
const { data: sub } = await supabaseServerClient
17
17
-
.from("user_subscriptions")
18
18
-
.select("stripe_subscription_id, current_period_end")
19
19
-
.eq("identity_id", identity.id)
20
20
-
.single();
21
21
-
22
22
-
if (!sub?.stripe_subscription_id) {
23
23
-
return Err("No active subscription found");
24
24
-
}
25
25
-
26
26
-
await getStripe().subscriptions.update(sub.stripe_subscription_id, {
27
27
-
cancel_at_period_end: false,
28
28
-
});
29
29
-
30
30
-
// Optimistic update
31
31
-
await supabaseServerClient
32
32
-
.from("user_subscriptions")
33
33
-
.update({
34
34
-
status: "active",
35
35
-
updated_at: new Date().toISOString(),
36
36
-
})
37
37
-
.eq("identity_id", identity.id);
38
38
-
39
39
-
return Ok({ renewsAt: sub.current_period_end || "" });
40
40
-
}
+2
-143
app/(home-pages)/home/Actions/AccountSettings.tsx
···
1
1
"use client";
2
2
3
3
-
import { useState } from "react";
4
4
-
import { mutate } from "swr";
5
3
import { ActionButton } from "components/ActionBar/ActionButton";
6
4
import { Popover } from "components/Popover";
7
5
import { ThemeSetterContent } from "components/ThemeManager/ThemeSetter";
8
6
import { useIsMobile } from "src/hooks/isMobile";
9
7
import { PaintSmall } from "components/Icons/PaintSmall";
10
10
-
import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";
11
11
-
import { GoBackSmall } from "components/Icons/GoBackSmall";
12
12
-
import { LogoutSmall } from "components/Icons/LogoutSmall";
13
13
-
import { ManageProSubscription } from "app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription";
14
14
-
import { Modal } from "components/Modal";
15
15
-
import { UpgradeContent } from "app/lish/[did]/[publication]/UpgradeModal";
16
16
-
import { useIsPro } from "src/hooks/useEntitlement";
17
8
18
9
export const AccountSettings = (props: { entityID: string }) => {
19
19
-
let [state, setState] = useState<
20
20
-
"menu" | "general" | "theme" | "manage-subscription"
21
21
-
>("menu");
22
10
let isMobile = useIsMobile();
23
11
24
12
return (
···
26
14
asChild
27
15
side={isMobile ? "top" : "right"}
28
16
align={isMobile ? "center" : "start"}
29
29
-
className={`w-xs bg-white!`}
17
17
+
className={`w-xs bg-white!`}
30
18
arrowFill="bg-white"
31
19
trigger={<ActionButton smallOnMobile icon=<PaintSmall /> label="Theme" />}
32
20
>
33
33
-
{state === "general" ? (
34
34
-
<GeneralSettings backToMenu={() => setState("menu")} />
35
35
-
) : state === "theme" ? (
36
36
-
<AccountThemeSettings
37
37
-
entityID={props.entityID}
38
38
-
backToMenu={() => setState("menu")}
39
39
-
/>
40
40
-
) : state === "manage-subscription" ? (
41
41
-
<ManageProSubscription backToMenu={() => setState("menu")} />
42
42
-
) : (
43
43
-
<SettingsMenu state={state} setState={setState} />
44
44
-
)}
21
21
+
<ThemeSetterContent entityID={props.entityID} home />
45
22
</Popover>
46
23
);
47
24
};
48
48
-
49
49
-
const SettingsMenu = (props: {
50
50
-
state: "menu" | "general" | "theme" | "manage-subscription";
51
51
-
setState: (s: typeof props.state) => void;
52
52
-
}) => {
53
53
-
let menuItemClassName =
54
54
-
"menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!";
55
55
-
56
56
-
let isPro = useIsPro();
57
57
-
58
58
-
return (
59
59
-
<div className="flex flex-col gap-0.5">
60
60
-
<AccountSettingsHeader state={"menu"} />
61
61
-
<button
62
62
-
className={menuItemClassName}
63
63
-
type="button"
64
64
-
onClick={() => {
65
65
-
props.setState("general");
66
66
-
}}
67
67
-
>
68
68
-
General
69
69
-
<ArrowRightTiny />
70
70
-
</button>
71
71
-
<button
72
72
-
className={menuItemClassName}
73
73
-
type="button"
74
74
-
onClick={() => props.setState("theme")}
75
75
-
>
76
76
-
Account Theme
77
77
-
<ArrowRightTiny />
78
78
-
</button>
79
79
-
{!isPro ? (
80
80
-
<Modal
81
81
-
trigger={
82
82
-
<div
83
83
-
className={`${menuItemClassName} bg-[var(--accent-light)]! border border-transparent hover:border-accent-contrast`}
84
84
-
>
85
85
-
Get Leaflet Pro
86
86
-
<ArrowRightTiny />{" "}
87
87
-
</div>
88
88
-
}
89
89
-
>
90
90
-
<UpgradeContent />
91
91
-
</Modal>
92
92
-
) : (
93
93
-
<button
94
94
-
className={`${menuItemClassName}`}
95
95
-
type="button"
96
96
-
onClick={() => props.setState("manage-subscription")}
97
97
-
>
98
98
-
Manage Pro Subscription <ArrowRightTiny />{" "}
99
99
-
</button>
100
100
-
)}
101
101
-
</div>
102
102
-
);
103
103
-
};
104
104
-
105
105
-
const GeneralSettings = (props: { backToMenu: () => void }) => {
106
106
-
return (
107
107
-
<div className="flex flex-col gap-0.5">
108
108
-
<AccountSettingsHeader
109
109
-
state={"general"}
110
110
-
backToMenuAction={() => props.backToMenu()}
111
111
-
/>
112
112
-
113
113
-
<button
114
114
-
className="flex gap-2 font-bold"
115
115
-
onClick={async () => {
116
116
-
await fetch("/api/auth/logout");
117
117
-
mutate("identity", null);
118
118
-
}}
119
119
-
>
120
120
-
<LogoutSmall />
121
121
-
Logout
122
122
-
</button>
123
123
-
</div>
124
124
-
);
125
125
-
};
126
126
-
const AccountThemeSettings = (props: {
127
127
-
entityID: string;
128
128
-
backToMenu: () => void;
129
129
-
}) => {
130
130
-
return (
131
131
-
<div className="flex flex-col gap-0.5">
132
132
-
<AccountSettingsHeader
133
133
-
state={"theme"}
134
134
-
backToMenuAction={() => props.backToMenu()}
135
135
-
/>
136
136
-
<ThemeSetterContent entityID={props.entityID} home />
137
137
-
</div>
138
138
-
);
139
139
-
};
140
140
-
export const AccountSettingsHeader = (props: {
141
141
-
state: "menu" | "general" | "theme";
142
142
-
backToMenuAction?: () => void;
143
143
-
}) => {
144
144
-
return (
145
145
-
<div className="flex justify-between font-bold text-secondary bg-border-light -mx-3 -mt-2 px-3 py-2 mb-1">
146
146
-
{props.state === "menu"
147
147
-
? "Settings"
148
148
-
: props.state === "general"
149
149
-
? "General"
150
150
-
: props.state === "theme"
151
151
-
? "Account Theme"
152
152
-
: ""}
153
153
-
{props.backToMenuAction && (
154
154
-
<button
155
155
-
type="button"
156
156
-
onClick={() => {
157
157
-
props.backToMenuAction && props.backToMenuAction();
158
158
-
}}
159
159
-
>
160
160
-
<GoBackSmall className="text-accent-contrast" />
161
161
-
</button>
162
162
-
)}
163
163
-
</div>
164
164
-
);
165
165
-
};
+32
-82
app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx
···
1
1
import { useState } from "react";
2
2
import { ButtonPrimary } from "components/Buttons";
3
3
-
import { PubSettingsHeader } from "./PublicationSettings";
4
4
-
import { cancelSubscription } from "actions/cancelSubscription";
5
5
-
import { reactivateSubscription } from "actions/reactivateSubscription";
3
3
+
import { createBillingPortalSession } from "actions/createBillingPortalSession";
6
4
import { useIdentityData } from "components/IdentityProvider";
7
5
import { DotLoader } from "components/utils/DotLoader";
8
6
import { useLocalizedDate } from "src/hooks/useLocalizedDate";
7
7
+
import { GoBackSmall } from "components/Icons/GoBackSmall";
9
8
10
9
export const ManageProSubscription = (props: { backToMenu: () => void }) => {
11
11
-
const [state, setState] = useState<"manage" | "confirm" | "success">(
12
12
-
"manage",
13
13
-
);
14
10
const [loading, setLoading] = useState(false);
15
11
const [error, setError] = useState<string | null>(null);
16
16
-
const { identity, mutate } = useIdentityData();
12
12
+
const { identity } = useIdentityData();
17
13
18
14
const subscription = identity?.subscription;
19
15
const renewalDate = useLocalizedDate(
···
21
17
{ month: "long", day: "numeric", year: "numeric" },
22
18
);
23
19
24
24
-
async function handleCancel() {
20
20
+
async function handleManageBilling() {
25
21
setLoading(true);
26
22
setError(null);
27
27
-
let result = await cancelSubscription();
23
23
+
const result = await createBillingPortalSession(window.location.href);
28
24
if (result.ok) {
29
29
-
setState("success");
30
30
-
mutate();
25
25
+
window.location.href = result.value.url;
31
26
} else {
32
27
setError(result.error);
28
28
+
setLoading(false);
33
29
}
34
34
-
setLoading(false);
35
35
-
}
36
36
-
37
37
-
async function handleReactivate() {
38
38
-
setLoading(true);
39
39
-
setError(null);
40
40
-
let result = await reactivateSubscription();
41
41
-
if (result.ok) {
42
42
-
mutate();
43
43
-
} else {
44
44
-
setError(result.error);
45
45
-
}
46
46
-
setLoading(false);
47
30
}
48
31
49
32
return (
50
33
<div>
51
51
-
<PubSettingsHeader backToMenuAction={props.backToMenu}>
34
34
+
<div className="flex justify-between font-bold text-secondary bg-border-light -mx-3 -mt-2 px-3 py-2 mb-1 flex-shrink-0">
52
35
Manage Subscription
53
53
-
</PubSettingsHeader>
36
36
+
<button type="button" onClick={props.backToMenu}>
37
37
+
<GoBackSmall className="text-accent-contrast" />
38
38
+
</button>
39
39
+
</div>
54
40
<div className="text-secondary text-center flex flex-col justify-center gap-2 py-2">
55
55
-
{state === "manage" && (
56
56
-
<>
57
57
-
<div>
58
58
-
You have a <br />
59
59
-
{subscription?.plan || "Pro"} subscription
60
60
-
<div className="text-lg font-bold text-primary">
61
61
-
{subscription?.plan || "Leaflet Pro"}
62
62
-
</div>
63
63
-
{subscription?.status === "canceled"
64
64
-
? "Your subscription has ended"
65
65
-
: subscription?.status === "canceling"
66
66
-
? `Access until ${renewalDate}`
67
67
-
: `Renews on ${renewalDate}`}
68
68
-
</div>
69
69
-
{subscription?.status === "canceling" && (
70
70
-
<ButtonPrimary
71
71
-
className="mx-auto"
72
72
-
compact
73
73
-
onClick={handleReactivate}
74
74
-
disabled={loading}
75
75
-
>
76
76
-
{loading ? <DotLoader /> : "Reactivate Subscription"}
77
77
-
</ButtonPrimary>
78
78
-
)}
79
79
-
{error && <div className="text-sm text-red-500 mt-2">{error}</div>}
80
80
-
{subscription?.status !== "canceling" &&
81
81
-
subscription?.status !== "canceled" && (
82
82
-
<ButtonPrimary
83
83
-
className="mx-auto"
84
84
-
compact
85
85
-
onClick={() => setState("confirm")}
86
86
-
>
87
87
-
Cancel Subscription
88
88
-
</ButtonPrimary>
89
89
-
)}
90
90
-
</>
91
91
-
)}
92
92
-
{state === "confirm" && (
93
93
-
<>
94
94
-
<div>Are you sure you'd like to cancel your subscription?</div>
95
95
-
<ButtonPrimary
96
96
-
className="mx-auto"
97
97
-
compact
98
98
-
onClick={handleCancel}
99
99
-
disabled={loading}
100
100
-
>
101
101
-
{loading ? <DotLoader /> : "Yes, Cancel it"}
102
102
-
</ButtonPrimary>
103
103
-
{error && <div className="text-sm text-red-500 mt-2">{error}</div>}
104
104
-
</>
105
105
-
)}
106
106
-
{state === "success" && (
107
107
-
<div>
108
108
-
Your subscription has been cancelled. You'll have access until{" "}
109
109
-
{renewalDate}.
41
41
+
<div>
42
42
+
You have a <br />
43
43
+
{subscription?.plan || "Pro"} subscription
44
44
+
<div className="text-lg font-bold text-primary">
45
45
+
{subscription?.plan || "Leaflet Pro"}
110
46
</div>
111
111
-
)}
47
47
+
{subscription?.status === "canceled"
48
48
+
? "Your subscription has ended"
49
49
+
: subscription?.status === "canceling"
50
50
+
? `Access until ${renewalDate}`
51
51
+
: `Renews on ${renewalDate}`}
52
52
+
</div>
53
53
+
<ButtonPrimary
54
54
+
className="mx-auto"
55
55
+
compact
56
56
+
onClick={handleManageBilling}
57
57
+
disabled={loading}
58
58
+
>
59
59
+
{loading ? <DotLoader /> : "Manage Billing"}
60
60
+
</ButtonPrimary>
61
61
+
{error && <div className="text-sm text-red-500 mt-2">{error}</div>}
112
62
</div>
113
63
</div>
114
64
);
+1
-35
app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx
···
13
13
import { DotLoader } from "components/utils/DotLoader";
14
14
import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";
15
15
import { PostOptions } from "./PostOptions";
16
16
-
import { UpgradeContent } from "../../UpgradeModal";
17
17
-
import { Modal } from "components/Modal";
18
18
-
import { ManageProSubscription } from "./ManageProSubscription";
19
19
-
import { useIsPro } from "src/hooks/useEntitlement";
20
16
21
21
-
type menuState =
22
22
-
| "menu"
23
23
-
| "pub-settings"
24
24
-
| "theme"
25
25
-
| "post-settings"
26
26
-
| "manage-subscription";
17
17
+
type menuState = "menu" | "pub-settings" | "theme" | "post-settings";
27
18
28
19
export function PublicationSettingsButton(props: { publication: string }) {
29
20
let isMobile = useIsMobile();
···
65
56
loading={loading}
66
57
setLoading={setLoading}
67
58
/>
68
68
-
) : state === "manage-subscription" ? (
69
69
-
<ManageProSubscription backToMenu={() => setState("menu")} />
70
59
) : (
71
60
<PubSettingsMenu
72
61
state={state}
···
88
77
}) => {
89
78
let menuItemClassName =
90
79
"menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!";
91
91
-
let isPro = useIsPro();
92
80
93
81
return (
94
82
<div className="flex flex-col gap-0.5">
···
119
107
Theme and Layout
120
108
<ArrowRightTiny />
121
109
</button>
122
122
-
{!isPro ? (
123
123
-
<Modal
124
124
-
trigger={
125
125
-
<div
126
126
-
className={`${menuItemClassName} bg-[var(--accent-light)]! border border-transparent hover:border-accent-contrast`}
127
127
-
>
128
128
-
Get Leaflet Pro
129
129
-
<ArrowRightTiny />{" "}
130
130
-
</div>
131
131
-
}
132
132
-
>
133
133
-
<UpgradeContent />
134
134
-
</Modal>
135
135
-
) : (
136
136
-
<button
137
137
-
className={`${menuItemClassName} `}
138
138
-
type="button"
139
139
-
onClick={() => props.setState("manage-subscription")}
140
140
-
>
141
141
-
Manage Pro Subscription <ArrowRightTiny />{" "}
142
142
-
</button>
143
143
-
)}
144
110
</div>
145
111
);
146
112
};
+68
-18
components/ActionBar/ProfileButton.tsx
···
3
3
import { useIdentityData } from "components/IdentityProvider";
4
4
import { AccountSmall } from "components/Icons/AccountSmall";
5
5
import { useRecordFromDid } from "src/utils/useRecordFromDid";
6
6
-
import { Menu, MenuItem } from "components/Menu";
7
6
import { useIsMobile } from "src/hooks/isMobile";
8
7
import { LogoutSmall } from "components/Icons/LogoutSmall";
9
8
import { mutate } from "swr";
10
9
import { SpeedyLink } from "components/SpeedyLink";
10
10
+
import { Popover } from "components/Popover";
11
11
+
import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";
12
12
+
import { Modal } from "components/Modal";
13
13
+
import { UpgradeContent } from "app/lish/[did]/[publication]/UpgradeModal";
14
14
+
import { ManageProSubscription } from "app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription";
15
15
+
import { useIsPro } from "src/hooks/useEntitlement";
16
16
+
import { useState } from "react";
11
17
12
18
export const ProfileButton = () => {
13
19
let { identity } = useIdentityData();
14
20
let { data: record } = useRecordFromDid(identity?.atp_did);
15
21
let isMobile = useIsMobile();
22
22
+
let [state, setState] = useState<"menu" | "manage-subscription">("menu");
23
23
+
let isPro = useIsPro();
16
24
17
25
return (
18
18
-
<Menu
26
26
+
<Popover
19
27
asChild
20
28
side={isMobile ? "top" : "right"}
21
29
align={isMobile ? "center" : "start"}
30
30
+
onOpenChange={() => setState("menu")}
31
31
+
className="w-xs"
22
32
trigger={
23
33
<ActionButton
24
34
nav
···
38
48
/>
39
49
}
40
50
>
41
41
-
{record && (
42
42
-
<>
43
43
-
<SpeedyLink className="no-underline!" href={`/p/${record.handle}`}>
44
44
-
<MenuItem onSelect={() => {}}>View Profile</MenuItem>
45
45
-
</SpeedyLink>
51
51
+
{state === "manage-subscription" ? (
52
52
+
<ManageProSubscription backToMenu={() => setState("menu")} />
53
53
+
) : (
54
54
+
<div className="flex flex-col gap-0.5">
55
55
+
{record && (
56
56
+
<>
57
57
+
<SpeedyLink
58
58
+
className="no-underline!"
59
59
+
href={`/p/${record.handle}`}
60
60
+
>
61
61
+
<button
62
62
+
type="button"
63
63
+
className="menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline! w-full"
64
64
+
>
65
65
+
View Profile
66
66
+
</button>
67
67
+
</SpeedyLink>
68
68
+
69
69
+
<hr className="border-border-light border-dashed" />
70
70
+
</>
71
71
+
)}
72
72
+
{!isPro ? (
73
73
+
<Modal
74
74
+
trigger={
75
75
+
<div className="menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline! bg-[var(--accent-light)]! border border-transparent hover:border-accent-contrast">
76
76
+
Get Leaflet Pro
77
77
+
<ArrowRightTiny />
78
78
+
</div>
79
79
+
}
80
80
+
>
81
81
+
<UpgradeContent />
82
82
+
</Modal>
83
83
+
) : (
84
84
+
<button
85
85
+
className="menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!"
86
86
+
type="button"
87
87
+
onClick={() => setState("manage-subscription")}
88
88
+
>
89
89
+
Manage Pro Subscription
90
90
+
<ArrowRightTiny />
91
91
+
</button>
92
92
+
)}
46
93
47
94
<hr className="border-border-light border-dashed" />
48
48
-
</>
95
95
+
96
96
+
<button
97
97
+
type="button"
98
98
+
className="menuItem -mx-[8px] text-left flex items-center gap-2 hover:no-underline!"
99
99
+
onClick={async () => {
100
100
+
await fetch("/api/auth/logout");
101
101
+
mutate("identity", null);
102
102
+
}}
103
103
+
>
104
104
+
<LogoutSmall />
105
105
+
Log Out
106
106
+
</button>
107
107
+
</div>
49
108
)}
50
50
-
<MenuItem
51
51
-
onSelect={async () => {
52
52
-
await fetch("/api/auth/logout");
53
53
-
mutate("identity", null);
54
54
-
}}
55
55
-
>
56
56
-
<LogoutSmall />
57
57
-
Log Out
58
58
-
</MenuItem>
59
59
-
</Menu>
109
109
+
</Popover>
60
110
);
61
111
};