This commit is contained in:
Nicolas Cantu 2026-01-15 12:12:05 +01:00
parent b792e5373a
commit bc557b9657
10 changed files with 700 additions and 33 deletions

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { useState } from 'react'
import { Card, ErrorState } from './ui' import { Card, ErrorState } from './ui'
import type { ArticleDraft } from '@/lib/articlePublisher' import type { ArticleDraft } from '@/lib/articlePublisher'
import { ArticleFormButtons } from './ArticleFormButtons' import { ArticleFormButtons } from './ArticleFormButtons'
@ -6,6 +6,7 @@ import type { RelayPublishStatus } from '@/lib/publishResult'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { ArticleFieldsLeft } from './ArticleEditorFormFieldsLeft' import { ArticleFieldsLeft } from './ArticleEditorFormFieldsLeft'
import { ArticleFieldsRight } from './ArticleEditorFormFieldsRight' import { ArticleFieldsRight } from './ArticleEditorFormFieldsRight'
import { ArticlePreviewModal } from './ArticlePreviewModal'
interface ArticleEditorFormProps { interface ArticleEditorFormProps {
draft: ArticleDraft draft: ArticleDraft
@ -26,6 +27,23 @@ function ErrorAlert({ error }: { error: string | null }): React.ReactElement | n
return <ErrorState message={error} className="bg-red-50 border-red-200 text-red-800" /> return <ErrorState message={error} className="bg-red-50 border-red-200 text-red-800" />
} }
function usePreviewModal(): {
showPreview: boolean
openPreview: () => void
closePreview: () => void
} {
const [showPreview, setShowPreview] = useState(false)
return {
showPreview,
openPreview: () => {
setShowPreview(true)
},
closePreview: () => {
setShowPreview(false)
},
}
}
export function ArticleEditorForm({ export function ArticleEditorForm({
draft, draft,
onDraftChange, onDraftChange,
@ -37,22 +55,32 @@ export function ArticleEditorForm({
seriesOptions, seriesOptions,
onSelectSeries, onSelectSeries,
}: ArticleEditorFormProps): React.ReactElement { }: ArticleEditorFormProps): React.ReactElement {
const { showPreview, openPreview, closePreview } = usePreviewModal()
return ( return (
<Card variant="default" className="bg-white"> <>
<form onSubmit={onSubmit} className="space-y-4"> <Card variant="default" className="bg-white">
<h2 className="text-2xl font-bold mb-4">{t('article.editor.title')}</h2> <form onSubmit={onSubmit} className="space-y-4">
<div className="space-y-4"> <h2 className="text-2xl font-bold mb-4">{t('article.editor.title')}</h2>
<ArticleFieldsLeft <div className="space-y-4">
draft={draft} <ArticleFieldsLeft
onDraftChange={onDraftChange} draft={draft}
{...(seriesOptions ? { seriesOptions } : {})} onDraftChange={onDraftChange}
{...(onSelectSeries ? { onSelectSeries } : {})} {...(seriesOptions ? { seriesOptions } : {})}
{...(onSelectSeries ? { onSelectSeries } : {})}
/>
<ArticleFieldsRight draft={draft} onDraftChange={onDraftChange} />
</div>
<ErrorAlert error={error} />
<ArticleFormButtons
loading={loading}
relayStatuses={relayStatuses}
onPreview={openPreview}
{...(onCancel ? { onCancel } : {})}
/> />
<ArticleFieldsRight draft={draft} onDraftChange={onDraftChange} /> </form>
</div> </Card>
<ErrorAlert error={error} /> <ArticlePreviewModal isOpen={showPreview} onClose={closePreview} draft={draft} />
<ArticleFormButtons loading={loading} relayStatuses={relayStatuses} {...(onCancel ? { onCancel } : {})} /> </>
</form>
</Card>
) )
} }

View File

@ -3,11 +3,12 @@ import { t } from '@/lib/i18n'
interface ArticleFormButtonsProps { interface ArticleFormButtonsProps {
loading: boolean loading: boolean
relayStatuses?: unknown // Kept for backward compatibility but not displayed relayStatuses?: unknown
onCancel?: () => void onCancel?: () => void
onPreview?: () => void
} }
export function ArticleFormButtons({ loading, onCancel }: ArticleFormButtonsProps): React.ReactElement { export function ArticleFormButtons({ loading, onCancel, onPreview }: ArticleFormButtonsProps): React.ReactElement {
return ( return (
<div className="space-y-3 pt-4"> <div className="space-y-3 pt-4">
<div className="flex gap-3"> <div className="flex gap-3">
@ -20,6 +21,16 @@ export function ArticleFormButtons({ loading, onCancel }: ArticleFormButtonsProp
> >
{loading ? t('publish.publishing') : t('publish.button')} {loading ? t('publish.publishing') : t('publish.button')}
</Button> </Button>
{onPreview && (
<Button
type="button"
variant="secondary"
onClick={onPreview}
disabled={loading}
>
{t('article.preview.button')}
</Button>
)}
{onCancel && ( {onCancel && (
<Button <Button
type="button" type="button"

View File

@ -120,6 +120,7 @@ function PageDisplay({ page }: { page: Page }): React.ReactElement {
src={page.content} src={page.content}
alt={t('page.image.alt', { number: page.number })} alt={t('page.image.alt', { number: page.number })}
className="max-w-full h-auto rounded border border-neon-cyan/20" className="max-w-full h-auto rounded border border-neon-cyan/20"
loading="lazy"
/> />
) : ( ) : (
<div className="text-center py-8 text-cyber-accent/50 border border-dashed border-neon-cyan/20 rounded"> <div className="text-center py-8 text-cyber-accent/50 border border-dashed border-neon-cyan/20 rounded">

View File

@ -0,0 +1,87 @@
import { Modal } from './ui'
import type { ArticleDraft } from '@/lib/articlePublisher'
import { t } from '@/lib/i18n'
interface ArticlePreviewModalProps {
isOpen: boolean
onClose: () => void
draft: ArticleDraft
}
export function ArticlePreviewModal({ isOpen, onClose, draft }: ArticlePreviewModalProps): React.ReactElement {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={t('article.preview.title')}
size="large"
>
<ArticlePreviewContent draft={draft} />
</Modal>
)
}
function ArticlePreviewContent({ draft }: { draft: ArticleDraft }): React.ReactElement {
return (
<div className="space-y-6">
<ArticlePreviewHeader draft={draft} />
<ArticlePreviewBody draft={draft} />
<ArticlePreviewMeta draft={draft} />
</div>
)
}
function ArticlePreviewHeader({ draft }: { draft: ArticleDraft }): React.ReactElement {
return (
<div className="border-b border-neon-cyan/30 pb-4">
<h2 className="text-2xl font-bold text-neon-cyan mb-2">{draft.title || t('article.preview.noTitle')}</h2>
{draft.category && (
<span className="text-sm text-cyber-accent/70">
{t(`category.${draft.category}`)}
</span>
)}
</div>
)
}
function ArticlePreviewBody({ draft }: { draft: ArticleDraft }): React.ReactElement {
return (
<div className="space-y-4">
{draft.preview && (
<div>
<h3 className="text-sm font-semibold text-neon-cyan mb-2">{t('article.preview.previewLabel')}</h3>
<p className="text-cyber-accent whitespace-pre-wrap">{draft.preview}</p>
</div>
)}
{draft.content && (
<div>
<h3 className="text-sm font-semibold text-neon-cyan mb-2">{t('article.preview.contentLabel')}</h3>
<p className="text-sm text-cyber-accent/80 whitespace-pre-wrap">{draft.content}</p>
</div>
)}
{(!draft.preview && !draft.content) && (
<p className="text-cyber-accent/50 italic">{t('article.preview.empty')}</p>
)}
</div>
)
}
function ArticlePreviewMeta({ draft }: { draft: ArticleDraft }): React.ReactElement {
return (
<div className="border-t border-neon-cyan/30 pt-4 text-xs text-cyber-accent/70 space-y-2">
<div>
<span className="font-semibold">{t('article.preview.zapAmount')}:</span> {draft.zapAmount} sats
</div>
{draft.seriesId && (
<div>
<span className="font-semibold">{t('article.preview.series')}:</span> {draft.seriesId}
</div>
)}
{draft.media && draft.media.length > 0 && (
<div>
<span className="font-semibold">{t('article.preview.media')}:</span> {draft.media.length} {t('article.preview.mediaItems')}
</div>
)}
</div>
)
}

View File

@ -1,9 +1,10 @@
import { useRef } from 'react' import { useRef } from 'react'
import type { Article } from '@/types/nostr' import type { Article } from '@/types/nostr'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'
import { ErrorState, EmptyState, Skeleton } from './ui' import { ErrorState, EmptyState, Skeleton, Button } from './ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { useArrowNavigation } from '@/hooks/useArrowNavigation' import { useArrowNavigation } from '@/hooks/useArrowNavigation'
import { usePagination } from '@/hooks/usePagination'
interface ArticlesListProps { interface ArticlesListProps {
articles: Article[] articles: Article[]
@ -14,6 +15,8 @@ interface ArticlesListProps {
unlockedArticles: Set<string> unlockedArticles: Set<string>
} }
const ITEMS_PER_PAGE = 10
function ArticleCardSkeleton(): React.ReactElement { function ArticleCardSkeleton(): React.ReactElement {
return ( return (
<div className="border border-neon-cyan/30 rounded-lg p-6 bg-cyber-dark space-y-4"> <div className="border border-neon-cyan/30 rounded-lg p-6 bg-cyber-dark space-y-4">
@ -69,7 +72,106 @@ function ArticlesErrorState({ error }: { error: string }): React.ReactElement {
) )
} }
function ArticlesContent({ function PaginationInfo({ currentPage, totalPages }: { currentPage: number; totalPages: number }): React.ReactElement {
const stepText = t('pagination.page', { current: currentPage, total: totalPages })
return (
<div className="text-sm text-cyber-accent/70">
{stepText}
</div>
)
}
function PaginationButtons({
onNext,
onPrevious,
hasNext,
hasPrevious,
}: {
onNext: () => void
onPrevious: () => void
hasNext: boolean
hasPrevious: boolean
}): React.ReactElement {
return (
<div className="flex gap-2">
<Button
type="button"
variant="secondary"
size="small"
onClick={onPrevious}
disabled={!hasPrevious}
>
{t('pagination.previous')}
</Button>
<Button
type="button"
variant="secondary"
size="small"
onClick={onNext}
disabled={!hasNext}
>
{t('pagination.next')}
</Button>
</div>
)
}
function PaginationControls({
currentPage,
totalPages,
onNext,
onPrevious,
hasNext,
hasPrevious,
}: {
currentPage: number
totalPages: number
onNext: () => void
onPrevious: () => void
hasNext: boolean
hasPrevious: boolean
}): React.ReactElement {
return (
<div className="flex items-center justify-between mt-6 pt-4 border-t border-neon-cyan/30">
<PaginationInfo currentPage={currentPage} totalPages={totalPages} />
<PaginationButtons
onNext={onNext}
onPrevious={onPrevious}
hasNext={hasNext}
hasPrevious={hasPrevious}
/>
</div>
)
}
function ArticlesListItems({
articles,
allArticles,
onUnlock,
unlockedArticles,
}: {
articles: Article[]
allArticles: Article[]
onUnlock: (article: Article) => void
unlockedArticles: Set<string>
}): React.ReactElement {
return (
<div className="space-y-6" role="list">
{articles.map((article) => (
<div key={article.id} role="listitem">
<ArticleCard
article={{ ...article, paid: unlockedArticles.has(article.id) || article.paid }}
onUnlock={onUnlock}
allArticles={allArticles}
unlockedArticles={unlockedArticles}
/>
</div>
))}
</div>
)
}
function ArticlesListContent({
articles, articles,
allArticles, allArticles,
onUnlock, onUnlock,
@ -82,23 +184,36 @@ function ArticlesContent({
unlockedArticles: Set<string> unlockedArticles: Set<string>
containerRef: React.RefObject<HTMLDivElement | null> containerRef: React.RefObject<HTMLDivElement | null>
}): React.ReactElement { }): React.ReactElement {
const pagination = usePagination({ items: articles, itemsPerPage: ITEMS_PER_PAGE })
const showingText = t('pagination.showing', {
current: pagination.paginatedItems.length,
total: articles.length,
all: allArticles.length,
})
return ( return (
<section id="articles-section" aria-label={t('navigation.articlesSection')} tabIndex={-1}> <section id="articles-section" aria-label={t('navigation.articlesSection')} tabIndex={-1}>
<div className="mb-4 text-sm text-cyber-accent/70"> <div className="mb-4 text-sm text-cyber-accent/70">
Showing {articles.length} of {allArticles.length} article{allArticles.length !== 1 ? 's' : ''} {showingText}
</div> </div>
<div ref={containerRef} className="space-y-6" role="list"> <div ref={containerRef}>
{articles.map((article) => ( <ArticlesListItems
<div key={article.id} role="listitem"> articles={pagination.paginatedItems}
<ArticleCard allArticles={allArticles}
article={{ ...article, paid: unlockedArticles.has(article.id) || article.paid }} onUnlock={onUnlock}
onUnlock={onUnlock} unlockedArticles={unlockedArticles}
allArticles={allArticles} />
unlockedArticles={unlockedArticles}
/>
</div>
))}
</div> </div>
{pagination.totalPages > 1 && (
<PaginationControls
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
onNext={pagination.nextPage}
onPrevious={pagination.previousPage}
hasNext={pagination.hasNextPage}
hasPrevious={pagination.hasPreviousPage}
/>
)}
</section> </section>
) )
} }
@ -125,7 +240,7 @@ export function ArticlesList({
} }
return ( return (
<ArticlesContent <ArticlesListContent
articles={articles} articles={articles}
allArticles={allArticles} allArticles={allArticles}
onUnlock={onUnlock} onUnlock={onUnlock}

View File

@ -8,6 +8,7 @@ import { AuthorsList } from '@/components/AuthorsList'
import { PageHeader } from '@/components/PageHeader' import { PageHeader } from '@/components/PageHeader'
import { Footer } from '@/components/Footer' import { Footer } from '@/components/Footer'
import { SkipLinks } from '@/components/SkipLinks' import { SkipLinks } from '@/components/SkipLinks'
import { OnboardingTour } from '@/components/OnboardingTour'
import type { Dispatch, SetStateAction } from 'react' import type { Dispatch, SetStateAction } from 'react'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
@ -166,6 +167,7 @@ export function HomeView(props: HomeViewProps): React.ReactElement {
<main role="main" className="min-h-screen bg-cyber-darker"> <main role="main" className="min-h-screen bg-cyber-darker">
<PageHeader /> <PageHeader />
<HomeContent {...props} /> <HomeContent {...props} />
<OnboardingTour />
<Footer /> <Footer />
</main> </main>
</> </>

View File

@ -0,0 +1,233 @@
import { useState, useEffect } from 'react'
import { Button, Modal } from './ui'
import { t } from '@/lib/i18n'
import { isOnboardingCompleted, markOnboardingCompleted } from '@/lib/onboardingPreferences'
interface OnboardingStep {
id: string
title: string
content: string
targetSelector?: string
}
const ONBOARDING_STEPS: OnboardingStep[] = [
{
id: 'welcome',
title: t('onboarding.steps.welcome.title'),
content: t('onboarding.steps.welcome.content'),
},
{
id: 'search',
title: t('onboarding.steps.search.title'),
content: t('onboarding.steps.search.content'),
targetSelector: '[role="search"]',
},
{
id: 'filters',
title: t('onboarding.steps.filters.title'),
content: t('onboarding.steps.filters.content'),
targetSelector: '#filters-section',
},
{
id: 'articles',
title: t('onboarding.steps.articles.title'),
content: t('onboarding.steps.articles.content'),
targetSelector: '#articles-section',
},
{
id: 'payment',
title: t('onboarding.steps.payment.title'),
content: t('onboarding.steps.payment.content'),
},
]
interface OnboardingTourProps {
onComplete?: () => void
}
interface OnboardingHandlersParams {
isLastStep: boolean
currentStep: number
setCurrentStep: (value: number) => void
setIsActive: (value: boolean) => void
onComplete?: () => void
}
function useOnboardingState(): {
isActive: boolean
setIsActive: (value: boolean) => void
currentStep: number
setCurrentStep: (value: number) => void
isLoading: boolean
} {
const [isActive, setIsActive] = useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const checkOnboarding = async (): Promise<void> => {
try {
const completed = await isOnboardingCompleted()
if (!completed) {
setIsActive(true)
}
} catch (error) {
console.error('Error checking onboarding status:', error)
} finally {
setIsLoading(false)
}
}
void checkOnboarding()
}, [])
return { isActive, setIsActive, currentStep, setCurrentStep, isLoading }
}
function useStepScrolling(isActive: boolean, currentStep: number): void {
useEffect(() => {
if (isActive && currentStep < ONBOARDING_STEPS.length) {
const step = ONBOARDING_STEPS[currentStep]
if (step?.targetSelector) {
const element = document.querySelector(step.targetSelector)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}
}
}, [isActive, currentStep])
}
function OnboardingTourActions({
currentStep,
isLastStep,
onNext,
onSkip,
}: {
currentStep: number
isLastStep: boolean
onNext: () => void
onSkip: () => void
}): React.ReactElement {
const stepText = t('onboarding.step', { current: currentStep + 1, total: ONBOARDING_STEPS.length })
return (
<div className="flex justify-between items-center">
<div className="text-xs text-cyber-accent/70">
{stepText}
</div>
<div className="flex gap-2">
<Button
type="button"
variant="ghost"
size="small"
onClick={onSkip}
>
{t('onboarding.skip')}
</Button>
<Button
type="button"
variant="primary"
size="small"
onClick={onNext}
>
{isLastStep ? t('onboarding.finish') : t('onboarding.next')}
</Button>
</div>
</div>
)
}
function OnboardingTourContent({
step,
currentStep,
isLastStep,
onNext,
onSkip,
}: {
step: OnboardingStep
currentStep: number
isLastStep: boolean
onNext: () => void
onSkip: () => void
}): React.ReactElement {
return (
<div className="space-y-4">
<p className="text-cyber-accent">{step.content}</p>
<OnboardingTourActions
currentStep={currentStep}
isLastStep={isLastStep}
onNext={onNext}
onSkip={onSkip}
/>
</div>
)
}
function useOnboardingHandlers(params: OnboardingHandlersParams): {
handleNext: () => void
handleSkip: () => void
} {
const handleComplete = async (): Promise<void> => {
try {
await markOnboardingCompleted()
params.setIsActive(false)
params.onComplete?.()
} catch (error) {
console.error('Error marking onboarding as completed:', error)
}
}
const handleNext = (): void => {
if (params.isLastStep) {
void handleComplete()
} else {
params.setCurrentStep(params.currentStep + 1)
}
}
const handleSkip = (): void => {
void handleComplete()
}
return { handleNext, handleSkip }
}
export function OnboardingTour({ onComplete }: OnboardingTourProps): React.ReactElement | null {
const { isActive, setIsActive, currentStep, setCurrentStep, isLoading } = useOnboardingState()
useStepScrolling(isActive, currentStep)
if (isLoading || !isActive) {
return null
}
const step = ONBOARDING_STEPS[currentStep]
if (!step) {
return null
}
const isLastStep = currentStep === ONBOARDING_STEPS.length - 1
const { handleNext, handleSkip } = useOnboardingHandlers({
isLastStep,
currentStep,
setCurrentStep,
setIsActive,
onComplete,
})
return (
<Modal
isOpen={isActive}
onClose={handleSkip}
title={step.title}
size="medium"
>
<OnboardingTourContent
step={step}
currentStep={currentStep}
isLastStep={isLastStep}
onNext={handleNext}
onSkip={handleSkip}
/>
</Modal>
)
}

View File

@ -0,0 +1,57 @@
import { useEffect, useRef, useState } from 'react'
interface UseInfiniteScrollOptions {
hasMore: boolean
loading: boolean
onLoadMore: () => void
threshold?: number
}
export function useInfiniteScroll({
hasMore,
loading,
onLoadMore,
threshold = 200,
}: UseInfiniteScrollOptions): React.RefObject<HTMLDivElement | null> {
const observerRef = useRef<HTMLDivElement>(null)
const [isIntersecting, setIsIntersecting] = useState(false)
useEffect(() => {
if (!hasMore || loading) {
return
}
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0]
if (entry?.isIntersecting) {
setIsIntersecting(true)
} else {
setIsIntersecting(false)
}
},
{
rootMargin: `${threshold}px`,
}
)
const currentRef = observerRef.current
if (currentRef) {
observer.observe(currentRef)
}
return () => {
if (currentRef) {
observer.unobserve(currentRef)
}
}
}, [hasMore, loading, threshold])
useEffect(() => {
if (isIntersecting && hasMore && !loading) {
onLoadMore()
}
}, [isIntersecting, hasMore, loading, onLoadMore])
return observerRef
}

60
hooks/usePagination.ts Normal file
View File

@ -0,0 +1,60 @@
import { useState, useMemo } from 'react'
interface UsePaginationOptions<T> {
items: T[]
itemsPerPage: number
}
interface UsePaginationResult<T> {
currentPage: number
totalPages: number
paginatedItems: T[]
goToPage: (page: number) => void
nextPage: () => void
previousPage: () => void
hasNextPage: boolean
hasPreviousPage: boolean
}
export function usePagination<T>({ items, itemsPerPage }: UsePaginationOptions<T>): UsePaginationResult<T> {
const [currentPage, setCurrentPage] = useState(1)
const totalPages = Math.max(1, Math.ceil(items.length / itemsPerPage))
const paginatedItems = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
return items.slice(startIndex, endIndex)
}, [items, currentPage, itemsPerPage])
const goToPage = (page: number): void => {
const validPage = Math.max(1, Math.min(page, totalPages))
setCurrentPage(validPage)
}
const nextPage = (): void => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1)
}
}
const previousPage = (): void => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1)
}
}
const hasNextPage = currentPage < totalPages
const hasPreviousPage = currentPage > 1
return {
currentPage,
totalPages,
paginatedItems,
goToPage,
nextPage,
previousPage,
hasNextPage,
hasPreviousPage,
}
}

View File

@ -0,0 +1,73 @@
import { createIndexedDBHelper, type IndexedDBHelper } from './helpers/indexedDBHelper'
const DB_NAME = 'nostr_paywall_settings'
const DB_VERSION = 3
const STORE_NAME = 'onboarding_preferences'
export interface OnboardingPreferencesItem {
key: 'completed'
value: boolean
timestamp: number
}
const COMPLETED_KEY = 'completed'
class OnboardingPreferencesService {
private readonly dbHelper: IndexedDBHelper
constructor() {
this.dbHelper = createIndexedDBHelper({
dbName: DB_NAME,
version: DB_VERSION,
storeName: STORE_NAME,
keyPath: 'key',
indexes: [{ name: 'timestamp', keyPath: 'timestamp', unique: false }],
})
}
async isCompleted(): Promise<boolean> {
try {
const result = await this.dbHelper.get<OnboardingPreferencesItem>(COMPLETED_KEY)
return result?.value ?? false
} catch (error) {
console.error('Error loading onboarding preferences:', error)
return false
}
}
async markCompleted(): Promise<void> {
try {
await this.dbHelper.put({
key: COMPLETED_KEY,
value: true,
timestamp: Date.now(),
})
} catch (error) {
console.error('Error saving onboarding preferences:', error)
throw error
}
}
async clear(): Promise<void> {
try {
await this.dbHelper.delete(COMPLETED_KEY)
} catch (error) {
console.error('Error clearing onboarding preferences:', error)
throw error
}
}
}
const onboardingPreferencesService = new OnboardingPreferencesService()
export async function isOnboardingCompleted(): Promise<boolean> {
return onboardingPreferencesService.isCompleted()
}
export async function markOnboardingCompleted(): Promise<void> {
return onboardingPreferencesService.markCompleted()
}
export async function clearOnboardingPreferences(): Promise<void> {
return onboardingPreferencesService.clear()
}