Skip to content

Commit b6b28bb

Browse files
committed
Refactor premium activation
1 parent fdeec38 commit b6b28bb

4 files changed

Lines changed: 83 additions & 63 deletions

File tree

src/app/components/Sidebar/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import minimalLogo from '~/assets/minimal-logo.svg'
1414
import { cx } from '~/utils'
1515
import { useEnabledBots } from '~app/hooks/use-enabled-bots'
1616
import { showDiscountModalAtom, sidebarCollapsedAtom } from '~app/state'
17-
import { hasLicenseInstance } from '~services/premium'
17+
import { getPremiumActivation } from '~services/premium'
1818
import * as api from '~services/server-api'
1919
import { getAppOpenTimes, getPremiumModalOpenTimes } from '~services/storage/open-times'
2020
import CommandBar from '../CommandBar'
@@ -44,7 +44,7 @@ function Sidebar() {
4444

4545
useEffect(() => {
4646
Promise.all([getAppOpenTimes(), getPremiumModalOpenTimes()]).then(async ([appOpenTimes, premiumModalOpenTimes]) => {
47-
if (hasLicenseInstance()) {
47+
if (getPremiumActivation()) {
4848
return
4949
}
5050
const { show } = await api.checkDiscount({ appOpenTimes, premiumModalOpenTimes })

src/app/hooks/use-premium.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,16 @@
1-
import { useAtom } from 'jotai'
21
import { FetchError } from 'ofetch'
32
import useSWR from 'swr'
4-
import { licenseKeyAtom } from '~app/state'
5-
import { clearLicenseInstances, getLicenseInstanceId, validateLicenseKey } from '~services/premium'
3+
import { getPremiumActivation, validatePremium } from '~services/premium'
64

75
export function usePremium() {
8-
const [licenseKey, setLicenseKey] = useAtom(licenseKeyAtom)
9-
10-
const activateQuery = useSWR<{ valid: true } | { valid: false; error?: string }>(
11-
`license:${licenseKey}`,
6+
const validationQuery = useSWR<{ valid: true } | { valid: false; error?: string }>(
7+
'premium-validation',
128
async () => {
13-
if (!licenseKey) {
14-
return { valid: false }
15-
}
169
try {
17-
return await validateLicenseKey(licenseKey)
10+
return await validatePremium()
1811
} catch (err) {
1912
if (err instanceof FetchError) {
2013
if (err.status === 404) {
21-
clearLicenseInstances()
22-
setLicenseKey('')
2314
return { valid: false }
2415
}
2516
if (err.status === 400) {
@@ -30,15 +21,15 @@ export function usePremium() {
3021
}
3122
},
3223
{
33-
fallbackData: getLicenseInstanceId(licenseKey) ? { valid: true } : undefined,
24+
fallbackData: getPremiumActivation() ? { valid: true } : undefined,
3425
revalidateOnFocus: false,
3526
dedupingInterval: 10 * 60 * 1000,
3627
},
3728
)
3829

3930
return {
40-
activated: activateQuery.data?.valid,
41-
isLoading: activateQuery.isLoading,
42-
error: activateQuery.data?.valid === true ? undefined : activateQuery.data?.error,
31+
activated: validationQuery.data?.valid,
32+
isLoading: validationQuery.isLoading,
33+
error: validationQuery.data?.valid === true ? undefined : validationQuery.data?.error,
4334
}
4435
}

src/app/pages/PremiumPage.tsx

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,57 @@
11
import { useSearch } from '@tanstack/react-router'
2-
import ConfettiExplosion from 'react-confetti-explosion'
3-
import { useAtom } from 'jotai'
2+
import { get as getPath } from 'lodash-es'
43
import { useCallback, useState } from 'react'
4+
import ConfettiExplosion from 'react-confetti-explosion'
55
import { Toaster } from 'react-hot-toast'
66
import { useTranslation } from 'react-i18next'
77
import Button from '~app/components/Button'
88
import DiscountBadge from '~app/components/Premium/DiscountBadge'
99
import FeatureList from '~app/components/Premium/FeatureList'
1010
import PriceSection from '~app/components/Premium/PriceSection'
1111
import { usePremium } from '~app/hooks/use-premium'
12+
import { useDiscountCode } from '~app/hooks/use-purchase-info'
1213
import { trackEvent } from '~app/plausible'
1314
import { premiumRoute } from '~app/router'
14-
import { licenseKeyAtom } from '~app/state'
15-
import { deactivateLicenseKey } from '~services/premium'
16-
import { useDiscountCode } from '~app/hooks/use-purchase-info'
15+
import { activatePremium, deactivatePremium } from '~services/premium'
1716

1817
function PremiumPage() {
1918
const { t } = useTranslation()
20-
const [licenseKey, setLicenseKey] = useAtom(licenseKeyAtom)
2119
const premiumState = usePremium()
20+
const [activating, setActivating] = useState(false)
2221
const [deactivating, setDeactivating] = useState(false)
22+
const [activationError, setActivationError] = useState('')
2323
const { source } = useSearch({ from: premiumRoute.id })
2424
const [isExploding, setIsExploding] = useState(false)
2525
const discountCode = useDiscountCode()
2626

27-
const activateLicense = useCallback(() => {
27+
const activate = useCallback(async () => {
2828
const key = window.prompt('Enter your license key', '')
29-
if (key) {
30-
setLicenseKey(key)
29+
if (!key) {
30+
return
3131
}
32-
}, [setLicenseKey])
33-
34-
const deactivateLicense = useCallback(async () => {
35-
if (!licenseKey) {
32+
setActivationError('')
33+
setActivating(true)
34+
trackEvent('activate_license')
35+
try {
36+
await activatePremium(key)
37+
} catch (err) {
38+
console.error('activation', err)
39+
setActivationError(getPath(err, 'data.error') || 'Activation failed')
40+
setActivating(false)
3641
return
3742
}
43+
setTimeout(() => location.reload(), 500)
44+
}, [])
45+
46+
const deactivateLicense = useCallback(async () => {
3847
if (!window.confirm('Are you sure to deactivate this device?')) {
3948
return
4049
}
4150
setDeactivating(true)
42-
await deactivateLicenseKey(licenseKey)
43-
setLicenseKey('')
51+
trackEvent('deactivate_license')
52+
await deactivatePremium()
4453
setTimeout(() => location.reload(), 500)
45-
}, [licenseKey, setLicenseKey])
54+
}, [])
4655

4756
return (
4857
<div className="flex flex-col bg-primary-background dark:text-primary-text rounded-[20px] h-full p-[50px] overflow-y-auto">
@@ -86,8 +95,8 @@ function PremiumPage() {
8695
text={t('Activate license')}
8796
color="flat"
8897
className="w-fit !py-2 rounded-lg"
89-
onClick={activateLicense}
90-
isLoading={premiumState.isLoading}
98+
onClick={activate}
99+
isLoading={activating || premiumState.isLoading}
91100
/>
92101
</>
93102
)}
@@ -100,7 +109,9 @@ function PremiumPage() {
100109
{t('Manage order and devices')}
101110
</a>
102111
</div>
103-
{!!premiumState.error && <span className="mt-3 text-red-500 font-medium">{premiumState.error}</span>}
112+
{!!(premiumState.error || activationError) && (
113+
<span className="mt-3 text-red-500 font-medium">{premiumState.error || activationError}</span>
114+
)}
104115
<Toaster position="top-right" />
105116
{isExploding && <ConfettiExplosion duration={3000} onComplete={() => setIsExploding(false)} />}
106117
</div>

src/services/premium.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,58 @@
11
import { getBrowser, getOS } from '~app/utils/navigator'
2-
import { activateLicense, deactivateLicense, validateLicense } from './lemonsqueezy'
2+
import * as lemonsqueezy from './lemonsqueezy'
3+
4+
interface PremiumActivation {
5+
licenseKey: string
6+
instanceId: string
7+
}
38

49
function getInstanceName() {
510
return `${getOS()} / ${getBrowser()}`
611
}
712

8-
export async function validateLicenseKey(key: string) {
9-
let instanceId = localStorage.getItem(`license_instance_id:${key}`)
10-
if (!instanceId) {
11-
instanceId = await activateLicense(key, getInstanceName())
12-
localStorage.setItem(`license_instance_id:${key}`, instanceId)
13-
}
14-
return validateLicense(key, instanceId)
13+
export async function activatePremium(licenseKey: string): Promise<PremiumActivation> {
14+
const instanceId = await lemonsqueezy.activateLicense(licenseKey, getInstanceName())
15+
const data = { licenseKey, instanceId }
16+
localStorage.setItem('premium', JSON.stringify(data))
17+
return data
1518
}
1619

17-
export async function deactivateLicenseKey(key: string) {
18-
const instanceId = localStorage.getItem(`license_instance_id:${key}`)
19-
if (!instanceId) {
20-
return
20+
export async function validatePremium() {
21+
const activation = getPremiumActivation()
22+
if (!activation) {
23+
return { valid: false }
2124
}
22-
await deactivateLicense(key, instanceId)
23-
localStorage.removeItem(`license_instance_id:${key}`)
25+
return lemonsqueezy.validateLicense(activation.licenseKey, activation.instanceId)
2426
}
2527

26-
export function getLicenseInstanceId(key: string) {
27-
return localStorage.getItem(`license_instance_id:${key}`)
28-
}
29-
30-
export function clearLicenseInstances() {
31-
for (const k of Object.keys(localStorage)) {
32-
if (k.startsWith('license_instance_id:')) {
33-
localStorage.removeItem(k)
34-
}
28+
export async function deactivatePremium() {
29+
const activation = getPremiumActivation()
30+
if (!activation) {
31+
return
3532
}
33+
await lemonsqueezy.deactivateLicense(activation.licenseKey, activation.instanceId)
34+
localStorage.removeItem('premium')
3635
}
3736

38-
export function hasLicenseInstance() {
39-
return Object.keys(localStorage).some((k) => k.startsWith('license_instance_id:'))
37+
export function getPremiumActivation(): PremiumActivation | null {
38+
const data = localStorage.getItem('premium')
39+
if (data) {
40+
return JSON.parse(data)
41+
}
42+
// Migrate old storage
43+
const key = localStorage.getItem('licenseKey')
44+
if (!key) {
45+
return null
46+
}
47+
const licenseKey: string = JSON.parse(key)
48+
const instanceId = localStorage.getItem(`license_instance_id:${licenseKey}`)
49+
if (!instanceId) {
50+
localStorage.removeItem('licenseKey')
51+
return null
52+
}
53+
const d = { licenseKey, instanceId }
54+
localStorage.setItem('premium', JSON.stringify(d))
55+
localStorage.removeItem('licenseKey')
56+
localStorage.removeItem(`license_instance_id:${licenseKey}`)
57+
return d
4058
}

0 commit comments

Comments
 (0)