chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { Skeleton } from '@sim/emcn'
|
||||
|
||||
export default function UnsubscribeLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<Skeleton className='size-[48px] rounded-[12px]' />
|
||||
<Skeleton className='mt-[16px] h-[24px] w-[180px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[8px] h-[14px] w-[300px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[4px] h-[14px] w-[260px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[24px] h-[44px] w-[200px] rounded-[10px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Unsubscribe from '@/app/unsubscribe/unsubscribe'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Unsubscribe',
|
||||
robots: { index: false },
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default Unsubscribe
|
||||
@@ -0,0 +1,234 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense } from 'react'
|
||||
import { Chip, cn, Loader } from '@sim/emcn'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import type { UnsubscribeType } from '@/lib/api/contracts/user'
|
||||
import { AuthSubmitButton } from '@/app/(auth)/components'
|
||||
import { AUTH_BUTTON_CLASS } from '@/app/(auth)/components/constants'
|
||||
import { InviteLayout } from '@/app/invite/components'
|
||||
import { useUnsubscribe, useUnsubscribeMutation } from '@/hooks/queries/unsubscribe'
|
||||
|
||||
function UnsubscribeContent() {
|
||||
const searchParams = useSearchParams()
|
||||
const email = searchParams.get('email')
|
||||
const token = searchParams.get('token')
|
||||
|
||||
const hasParams = Boolean(email) && Boolean(token)
|
||||
const query = useUnsubscribe(email ?? undefined, token ?? undefined)
|
||||
const unsubscribe = useUnsubscribeMutation()
|
||||
|
||||
const data = query.data ?? null
|
||||
const loading = hasParams && query.isLoading
|
||||
const processing = unsubscribe.isPending
|
||||
const unsubscribed = unsubscribe.isSuccess
|
||||
const error = !hasParams
|
||||
? 'Missing email or token in URL'
|
||||
: query.isError
|
||||
? getErrorMessage(query.error, 'Failed to validate unsubscribe link')
|
||||
: unsubscribe.isError
|
||||
? getErrorMessage(unsubscribe.error, 'Failed to process unsubscribe request')
|
||||
: null
|
||||
|
||||
const handleUnsubscribe = (type: UnsubscribeType) => {
|
||||
if (!email || !token) return
|
||||
unsubscribe.mutate({ email, token, type })
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<InviteLayout>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className={'font-medium text-[32px] text-[var(--text-primary)] tracking-tight'}>
|
||||
Loading
|
||||
</h1>
|
||||
<p className={'font-[380] text-[var(--text-muted)] text-md'}>
|
||||
Validating your unsubscribe link…
|
||||
</p>
|
||||
</div>
|
||||
<div className={'mt-8 flex w-full items-center justify-center py-8'}>
|
||||
<Loader className='size-8 text-[var(--text-muted)]' animate />
|
||||
</div>
|
||||
</InviteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<InviteLayout>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className={'font-medium text-[32px] text-[var(--text-primary)] tracking-tight'}>
|
||||
Invalid Unsubscribe Link
|
||||
</h1>
|
||||
<p className={'font-[380] text-[var(--text-muted)] text-md'}>{error}</p>
|
||||
</div>
|
||||
|
||||
<div className={'mt-8 w-full max-w-[410px] space-y-3'}>
|
||||
<AuthSubmitButton type='button' onClick={() => window.history.back()} loadingLabel=''>
|
||||
Go Back
|
||||
</AuthSubmitButton>
|
||||
</div>
|
||||
</InviteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
if (data?.isTransactional) {
|
||||
return (
|
||||
<InviteLayout>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className={'font-medium text-[32px] text-[var(--text-primary)] tracking-tight'}>
|
||||
Important Account Emails
|
||||
</h1>
|
||||
<p className={'font-[380] text-[var(--text-muted)] text-md'}>
|
||||
Transactional emails like password resets, account confirmations, and security alerts
|
||||
cannot be unsubscribed from as they contain essential information for your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={'mt-8 w-full max-w-[410px] space-y-3'}>
|
||||
<AuthSubmitButton type='button' onClick={() => window.close()} loadingLabel=''>
|
||||
Close
|
||||
</AuthSubmitButton>
|
||||
</div>
|
||||
</InviteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
if (unsubscribed) {
|
||||
return (
|
||||
<InviteLayout>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className={'font-medium text-[32px] text-[var(--text-primary)] tracking-tight'}>
|
||||
Successfully Unsubscribed
|
||||
</h1>
|
||||
<p className={'font-[380] text-[var(--text-muted)] text-md'}>
|
||||
You have been unsubscribed from our emails. You will stop receiving emails within 48
|
||||
hours.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={'mt-8 w-full max-w-[410px] space-y-3'}>
|
||||
<AuthSubmitButton type='button' onClick={() => window.close()} loadingLabel=''>
|
||||
Close
|
||||
</AuthSubmitButton>
|
||||
</div>
|
||||
</InviteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
const isAlreadyUnsubscribedFromAll = data?.currentPreferences.unsubscribeAll
|
||||
|
||||
return (
|
||||
<InviteLayout>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className={'font-medium text-[32px] text-[var(--text-primary)] tracking-tight'}>
|
||||
Email Preferences
|
||||
</h1>
|
||||
<p className={'font-[380] text-[var(--text-muted)] text-md'}>
|
||||
Choose which emails you'd like to stop receiving.
|
||||
</p>
|
||||
<p className={'mt-2 font-[380] text-[var(--text-muted)] text-sm'}>{data?.email}</p>
|
||||
</div>
|
||||
|
||||
<div className={'mt-8 w-full max-w-[410px] space-y-3'}>
|
||||
<AuthSubmitButton
|
||||
type='button'
|
||||
onClick={() => handleUnsubscribe('all')}
|
||||
disabled={isAlreadyUnsubscribedFromAll}
|
||||
loading={processing}
|
||||
loadingLabel='Unsubscribing…'
|
||||
>
|
||||
{isAlreadyUnsubscribedFromAll
|
||||
? 'Unsubscribed from All Emails'
|
||||
: 'Unsubscribe from All Marketing Emails'}
|
||||
</AuthSubmitButton>
|
||||
|
||||
<div className='py-2 text-center'>
|
||||
<span className={'font-[380] text-[var(--text-muted)] text-sm'}>
|
||||
or choose specific types
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Chip
|
||||
fullWidth
|
||||
flush
|
||||
onClick={() => handleUnsubscribe('marketing')}
|
||||
disabled={
|
||||
processing ||
|
||||
isAlreadyUnsubscribedFromAll ||
|
||||
data?.currentPreferences.unsubscribeMarketing
|
||||
}
|
||||
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
|
||||
>
|
||||
{data?.currentPreferences.unsubscribeMarketing
|
||||
? 'Unsubscribed from Marketing'
|
||||
: 'Unsubscribe from Marketing Emails'}
|
||||
</Chip>
|
||||
|
||||
<Chip
|
||||
fullWidth
|
||||
flush
|
||||
onClick={() => handleUnsubscribe('updates')}
|
||||
disabled={
|
||||
processing ||
|
||||
isAlreadyUnsubscribedFromAll ||
|
||||
data?.currentPreferences.unsubscribeUpdates
|
||||
}
|
||||
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
|
||||
>
|
||||
{data?.currentPreferences.unsubscribeUpdates
|
||||
? 'Unsubscribed from Updates'
|
||||
: 'Unsubscribe from Product Updates'}
|
||||
</Chip>
|
||||
|
||||
<Chip
|
||||
fullWidth
|
||||
flush
|
||||
onClick={() => handleUnsubscribe('notifications')}
|
||||
disabled={
|
||||
processing ||
|
||||
isAlreadyUnsubscribedFromAll ||
|
||||
data?.currentPreferences.unsubscribeNotifications
|
||||
}
|
||||
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
|
||||
>
|
||||
{data?.currentPreferences.unsubscribeNotifications
|
||||
? 'Unsubscribed from Notifications'
|
||||
: 'Unsubscribe from Notifications'}
|
||||
</Chip>
|
||||
</div>
|
||||
|
||||
<div className={'mt-6 max-w-[410px] text-center'}>
|
||||
<p className='font-[380] text-[var(--text-muted)] text-small'>
|
||||
You'll continue receiving important account emails like password resets and security
|
||||
alerts.
|
||||
</p>
|
||||
</div>
|
||||
</InviteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Unsubscribe() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<InviteLayout>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className={'font-medium text-[32px] text-[var(--text-primary)] tracking-tight'}>
|
||||
Loading
|
||||
</h1>
|
||||
<p className={'font-[380] text-[var(--text-muted)] text-md'}>
|
||||
Validating your unsubscribe link…
|
||||
</p>
|
||||
</div>
|
||||
<div className={'mt-8 flex w-full items-center justify-center py-8'}>
|
||||
<Loader className='size-8 text-[var(--text-muted)]' animate />
|
||||
</div>
|
||||
</InviteLayout>
|
||||
}
|
||||
>
|
||||
<UnsubscribeContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user