11import { openUrl } from "@tauri-apps/plugin-opener" ;
2- import { Check , ExternalLinkIcon } from "lucide-react" ;
2+ import { ExternalLinkIcon } from "lucide-react" ;
33import { type ReactNode , useCallback } from "react" ;
4+ import type Stripe from "stripe" ;
45
56import { Button } from "@hypr/ui/components/ui/button" ;
67
78import { useAuth } from "../../auth" ;
8- import { type BillingAccess , useBillingAccess } from "../../billing" ;
9+ import { useBillingAccess } from "../../billing" ;
910import { env } from "../../env" ;
1011
1112const WEB_APP_BASE_URL = env . VITE_APP_URL ?? "http://localhost:3000" ;
@@ -20,74 +21,91 @@ export function SettingsAccount() {
2021 openUrl ( `${ WEB_APP_BASE_URL } /app/account` ) ;
2122 } , [ ] ) ;
2223
23- const handleUpgrade = useCallback ( ( ) => {
24- openUrl ( `${ WEB_APP_BASE_URL } /app/checkout?period=monthly` ) ;
25- } , [ ] ) ;
26-
2724 const handleSignIn = useCallback ( ( ) => {
2825 auth ?. signIn ( ) ;
2926 } , [ auth ] ) ;
3027
3128 if ( ! isAuthenticated ) {
3229 return (
3330 < Container
34- title = "Sign in to manage your account "
35- description = "Sign in with your Hyprnote account to access billing and plan details ."
31+ title = "Sign in to Hyprnote "
32+ description = "Hyprnote account is required to access pro plan."
3633 action = {
3734 < Button onClick = { handleSignIn } >
38- < span > Sign in </ span >
35+ < span > Get Started </ span >
3936 </ Button >
4037 }
41- >
42- < p className = "text-sm text-neutral-600" >
43- The desktop app links directly to your web account for settings and
44- billing changes.
45- </ p >
46- </ Container >
38+ > </ Container >
4739 ) ;
4840 }
4941
5042 const hasStripeCustomer = ! ! billing . data ?. stripe_customer ;
51- const userEmail = auth ?. session ?. user ?. email ;
52- const userId = auth ?. session ?. user ?. id ;
5343
5444 return (
5545 < div className = "flex flex-col gap-4" >
5646 < Container
5747 title = "Your Account"
5848 description = "Redirect to the web app to manage your account."
5949 action = {
60- < Button variant = "outline" onClick = { handleOpenAccount } >
61- < span > Open</ span >
50+ < Button
51+ variant = "outline"
52+ onClick = { handleOpenAccount }
53+ className = "w-[100px] flex flex-row gap-1.5"
54+ >
55+ < span className = "text-sm" > Open</ span >
6256 < ExternalLinkIcon className = "text-neutral-600" />
6357 </ Button >
6458 }
65- >
66- < AccountDetails email = { userEmail } userId = { userId } />
67- </ Container >
59+ > </ Container >
6860
6961 < Container
7062 title = "Plan & Billing"
7163 description = "View your current plan and manage billing on the web."
7264 action = {
7365 hasStripeCustomer ? (
74- < Button variant = "outline" onClick = { handleOpenAccount } >
75- < span > Open</ span >
76- < ExternalLinkIcon className = "text-neutral-600" />
66+ < Button
67+ variant = "outline"
68+ onClick = { handleOpenAccount }
69+ className = "w-[100px] flex flex-row gap-1.5"
70+ >
71+ < span className = "text-sm" > Manage</ span >
72+ < ExternalLinkIcon className = "text-neutral-600" size = { 12 } />
7773 </ Button >
7874 ) : undefined
7975 }
8076 >
81- < SettingsBilling
82- billing = { billing }
83- onManage = { handleOpenAccount }
84- onUpgrade = { handleUpgrade }
85- />
77+ { billing . data ?. stripe_subscription && (
78+ < SubscriptionDetails
79+ subscription = { billing . data . stripe_subscription }
80+ />
81+ ) }
8682 </ Container >
8783 </ div >
8884 ) ;
8985}
9086
87+ function SubscriptionDetails ( {
88+ subscription,
89+ } : {
90+ subscription : Stripe . Subscription ;
91+ } ) {
92+ const {
93+ status,
94+ items : {
95+ data : [ { current_period_end, current_period_start } ] ,
96+ } ,
97+ } = subscription ;
98+
99+ return (
100+ < div className = "flex flex-row gap-1 text-xs text-neutral-600" >
101+ < span className = "capitalize" > { status } :</ span >
102+ < span > { new Date ( current_period_start * 1000 ) . toLocaleDateString ( ) } </ span >
103+ < span > ~</ span >
104+ < span > { new Date ( current_period_end * 1000 ) . toLocaleDateString ( ) } </ span >
105+ </ div >
106+ ) ;
107+ }
108+
91109function Container ( {
92110 title,
93111 description,
@@ -112,166 +130,3 @@ function Container({
112130 </ section >
113131 ) ;
114132}
115-
116- function AccountDetails ( {
117- email,
118- userId,
119- } : {
120- email ?: string | null ;
121- userId ?: string | null ;
122- } ) {
123- return (
124- < div className = "flex flex-col gap-3" >
125- < div >
126- < p className = "text-xs uppercase text-neutral-500" > Email</ p >
127- < p className = "text-sm text-neutral-900" >
128- { email ?? "Email unavailable" }
129- </ p >
130- </ div >
131-
132- { userId ? (
133- < div >
134- < p className = "text-xs uppercase text-neutral-500" > User ID</ p >
135- < p className = "font-mono text-xs text-neutral-500 break-all" >
136- { userId }
137- </ p >
138- </ div >
139- ) : null }
140- </ div >
141- ) ;
142- }
143-
144- function SettingsBilling ( {
145- billing,
146- onManage,
147- onUpgrade,
148- } : {
149- billing : BillingAccess ;
150- onManage : ( ) => void ;
151- onUpgrade : ( ) => void ;
152- } ) {
153- if ( billing . isPending && ! billing . data ) {
154- return (
155- < div className = "text-sm text-neutral-600" > Loading billing details...</ div >
156- ) ;
157- }
158-
159- const billingData = billing . data ;
160- const planId : PlanId = billingData ?. isPro ? "pro" : "free" ;
161- const plan = PLANS [ planId ] ;
162- const hasStripeCustomer = ! ! billingData ?. stripe_customer ;
163- const subscriptionStatus = billingData ?. stripe_subscription ?. status ;
164- const showErrorBanner = billing . isError ;
165- const errorMessage =
166- billing . error instanceof Error
167- ? billing . error . message
168- : "Unable to load billing details." ;
169-
170- return (
171- < div className = "space-y-4" >
172- { showErrorBanner && (
173- < div className = "rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between" >
174- < span > { errorMessage } </ span >
175- < Button
176- variant = "outline"
177- size = "sm"
178- onClick = { ( ) => billing . refetch ( ) }
179- disabled = { billing . isRefetching }
180- >
181- Retry
182- </ Button >
183- </ div >
184- ) }
185-
186- < div className = "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between" >
187- < div className = "flex-1" >
188- < p className = "text-xs uppercase text-neutral-500" > Active plan</ p >
189- < p className = "text-lg font-semibold text-neutral-900" > { plan . name } </ p >
190- < p className = "text-sm text-neutral-600" > { plan . description } </ p >
191- { subscriptionStatus && planId === "pro" ? (
192- < p className = "text-xs text-neutral-500" >
193- Subscription status:{ " " }
194- { formatSubscriptionStatus ( subscriptionStatus ) }
195- </ p >
196- ) : null }
197- </ div >
198-
199- < div className = "sm:w-auto" >
200- < PlanActions
201- planId = { planId }
202- hasStripeCustomer = { hasStripeCustomer }
203- onManage = { onManage }
204- onUpgrade = { onUpgrade }
205- />
206- </ div >
207- </ div >
208-
209- < ul className = "space-y-2" >
210- { plan . features . map ( ( feature ) => (
211- < li
212- key = { feature }
213- className = "flex items-start gap-2 text-sm text-neutral-700"
214- >
215- < Check size = { 16 } className = "mt-0.5 text-emerald-500 shrink-0" />
216- < span > { feature } </ span >
217- </ li >
218- ) ) }
219- </ ul >
220- </ div >
221- ) ;
222- }
223-
224- function PlanActions ( {
225- planId,
226- hasStripeCustomer,
227- onManage,
228- onUpgrade,
229- } : {
230- planId : PlanId ;
231- hasStripeCustomer : boolean ;
232- onManage : ( ) => void ;
233- onUpgrade : ( ) => void ;
234- } ) {
235- if ( planId === "pro" && hasStripeCustomer ) {
236- return (
237- < Button variant = "outline" onClick = { onManage } className = "w-full sm:w-auto" >
238- Manage billing
239- </ Button >
240- ) ;
241- }
242-
243- return (
244- < Button onClick = { onUpgrade } className = "w-full sm:w-auto" >
245- Upgrade to Pro
246- </ Button >
247- ) ;
248- }
249-
250- type PlanId = "free" | "pro" ;
251-
252- interface BillingPlan {
253- id : PlanId ;
254- name : string ;
255- description : string ;
256- features : string [ ] ;
257- }
258-
259- const PLANS : Record < PlanId , BillingPlan > = {
260- free : {
261- id : "free" ,
262- name : "Free" ,
263- description : "Local transcription with manual exports." ,
264- features : [ "Local transcription" , "Copy and PDF export" ] ,
265- } ,
266- pro : {
267- id : "pro" ,
268- name : "Pro" ,
269- description : "Cloud transcription, collaboration, and sharing features." ,
270- features : [ "Cloud transcription" , "Shareable links" ] ,
271- } ,
272- } ;
273-
274- function formatSubscriptionStatus ( status : string ) {
275- const normalized = status . replace ( / _ / g, " " ) ;
276- return normalized . charAt ( 0 ) . toUpperCase ( ) + normalized . slice ( 1 ) ;
277- }
0 commit comments