├── src ├── app │ ├── storybook │ │ ├── index.tsx │ │ └── _layout.tsx │ ├── (logged) │ │ ├── (tabs) │ │ │ ├── books.tsx │ │ │ ├── home.tsx │ │ │ └── account.tsx │ │ ├── books │ │ │ └── [id].tsx │ │ └── _layout.tsx │ ├── (public) │ │ ├── sign-in.tsx │ │ ├── otp-verification.tsx │ │ └── _layout.tsx │ ├── +not-found.tsx │ ├── index.tsx │ └── _layout.tsx ├── assets │ └── images │ │ ├── icon.png │ │ ├── logo-dark.png │ │ ├── logo-light.png │ │ ├── splash-icon-dark.png │ │ ├── splash-icon-light.png │ │ ├── android-icon-foreground.png │ │ └── android-icon-monochrome.png ├── features │ ├── app-onboarding │ │ ├── mascot.png │ │ ├── layout-login-image.jpg │ │ ├── store.ts │ │ ├── use-background-animated-style.tsx │ │ └── use-mascot-animated-style.tsx │ ├── devtools │ │ └── devtools.tsx │ ├── books │ │ └── book-cover.tsx │ └── auth │ │ ├── auth-header.tsx │ │ └── client.ts ├── locales │ ├── index.ts │ ├── en │ │ ├── books.json │ │ ├── layout.json │ │ ├── home.json │ │ ├── common.json │ │ ├── account.json │ │ ├── app-onboarding.json │ │ ├── index.ts │ │ └── auth.json │ └── fr │ │ ├── books.json │ │ ├── layout.json │ │ ├── common.json │ │ ├── home.json │ │ ├── account.json │ │ ├── app-onboarding.json │ │ ├── index.ts │ │ └── auth.json ├── components │ ├── keep-awake.tsx │ ├── ui │ │ ├── image.tsx │ │ ├── full-loader.tsx │ │ ├── sonner.tsx │ │ ├── card.tsx │ │ └── animated-step-indicator.tsx │ ├── version.tsx │ ├── icons │ │ ├── svg-sources │ │ │ ├── icon-panel.svg │ │ │ ├── icon-plus.svg │ │ │ ├── icon-check.svg │ │ │ ├── icon-chevron-down.svg │ │ │ ├── icon-chevron-left.svg │ │ │ ├── icon-chevron-right.svg │ │ │ ├── icon-chevron-up.svg │ │ │ ├── icon-play.svg │ │ │ ├── icon-arrow-down.svg │ │ │ ├── icon-arrow-up.svg │ │ │ ├── icon-credit-card.svg │ │ │ ├── icon-more-vertical.svg │ │ │ ├── icon-arrow-left.svg │ │ │ ├── icon-arrow-right.svg │ │ │ ├── icon-clock.svg │ │ │ ├── icon-dashboard.svg │ │ │ ├── icon-check-circle.svg │ │ │ ├── icon-search.svg │ │ │ ├── icon-x.svg │ │ │ ├── icon-chevrons-left.svg │ │ │ ├── icon-chevrons-right.svg │ │ │ ├── icon-alert-circle.svg │ │ │ ├── icon-chevrons-up-down.svg │ │ │ ├── icon-house-fill.svg │ │ │ ├── icon-user.svg │ │ │ ├── icon-lock.svg │ │ │ ├── icon-folder.svg │ │ │ ├── icon-home.svg │ │ │ ├── icon-mail.svg │ │ │ ├── icon-calendar.svg │ │ │ ├── icon-file.svg │ │ │ ├── icon-repository.svg │ │ │ ├── icon-x-circle.svg │ │ │ ├── icon-house.svg │ │ │ ├── icon-circle-user-round.svg │ │ │ ├── icon-copy.svg │ │ │ ├── icon-book-open.svg │ │ │ ├── icon-hash.svg │ │ │ ├── icon-trash-2.svg │ │ │ ├── icon-devices.svg │ │ │ ├── icon-log-out.svg │ │ │ ├── icon-download.svg │ │ │ ├── icon-log-in.svg │ │ │ ├── icon-share.svg │ │ │ ├── icon-upload.svg │ │ │ ├── icon-unlock.svg │ │ │ ├── icon-tag.svg │ │ │ ├── icon-external-link.svg │ │ │ ├── icon-languages.svg │ │ │ ├── icon-power.svg │ │ │ ├── icon-edit-3.svg │ │ │ ├── icon-book-open-fill.svg │ │ │ ├── icon-shopping-cart.svg │ │ │ ├── icon-moon.svg │ │ │ ├── icon-eye.svg │ │ │ ├── icon-eye-closed.svg │ │ │ ├── icon-house-duotone.svg │ │ │ ├── icon-share-2.svg │ │ │ ├── icon-book-open-duotone.svg │ │ │ ├── icon-user-circle-fill.svg │ │ │ ├── icon-sliders.svg │ │ │ └── icon-key.svg │ │ ├── icon.tsx │ │ ├── docs.stories.tsx │ │ ├── generated │ │ │ ├── IconPanel.tsx │ │ │ ├── IconCheck.tsx │ │ │ ├── IconChevronUp.tsx │ │ │ ├── IconPlay.tsx │ │ │ ├── IconChevronDown.tsx │ │ │ ├── IconChevronLeft.tsx │ │ │ ├── IconChevronRight.tsx │ │ │ ├── IconPlus.tsx │ │ │ ├── IconArrowDown.tsx │ │ │ ├── IconArrowUp.tsx │ │ │ ├── IconCreditCard.tsx │ │ │ ├── IconDashboard.tsx │ │ │ ├── IconCheckCircle.tsx │ │ │ ├── IconChevronsLeft.tsx │ │ │ ├── IconChevronsRight.tsx │ │ │ ├── IconArrowLeft.tsx │ │ │ ├── IconArrowRight.tsx │ │ │ ├── IconClock.tsx │ │ │ ├── IconSearch.tsx │ │ │ ├── IconX.tsx │ │ │ ├── IconMoreVertical.tsx │ │ │ ├── IconChevronsUpDown.tsx │ │ │ ├── IconHouseFill.tsx │ │ │ ├── IconFolder.tsx │ │ │ ├── IconAlertCircle.tsx │ │ │ ├── IconUser.tsx │ │ │ ├── IconLock.tsx │ │ │ ├── IconRepository.tsx │ │ │ ├── IconHome.tsx │ │ │ ├── IconMail.tsx │ │ │ ├── IconHouse.tsx │ │ │ ├── IconFile.tsx │ │ │ ├── IconCircleUserRound.tsx │ │ │ ├── IconTrash2.tsx │ │ │ ├── IconDevices.tsx │ │ │ ├── IconCopy.tsx │ │ │ ├── IconBookOpen.tsx │ │ │ ├── IconXCircle.tsx │ │ │ ├── IconCalendar.tsx │ │ │ ├── IconUnlock.tsx │ │ │ ├── IconTag.tsx │ │ │ ├── IconHash.tsx │ │ │ ├── IconLogOut.tsx │ │ │ ├── IconLanguages.tsx │ │ │ ├── IconLogIn.tsx │ │ │ ├── IconShare.tsx │ │ │ ├── IconUpload.tsx │ │ │ ├── IconDownload.tsx │ │ │ └── IconPower.tsx │ │ └── svgr.config.cjs │ ├── haptic-tab.tsx │ ├── theme-manager.tsx │ └── form │ │ └── docs.stories.tsx ├── lib │ ├── ficus-ui │ │ ├── foundations │ │ │ ├── font-sizes.ts │ │ │ ├── fonts.ts │ │ │ ├── colors.ts │ │ │ └── index.ts │ │ ├── theme.ts │ │ └── components │ │ │ ├── divider.ts │ │ │ ├── text.ts │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── badge.ts │ │ │ └── button.ts │ ├── hey-api │ │ ├── api.ts │ │ └── config.ts │ ├── tanstack-form │ │ ├── components │ │ │ ├── form-field-helper.tsx │ │ │ ├── form.tsx │ │ │ ├── form-field-label.tsx │ │ │ ├── index.ts │ │ │ ├── form-submit.tsx │ │ │ ├── form-field.tsx │ │ │ └── form-field-error.tsx │ │ └── context.ts │ └── i18n │ │ ├── constants.ts │ │ ├── config.ts │ │ └── index.ts ├── constants │ └── device.ts ├── layout │ ├── view-safe-content.tsx │ ├── splash-screen-manager.tsx │ └── view-tab-content.tsx ├── hooks │ ├── use-browser │ │ ├── docs.stories.tsx │ │ └── index.tsx │ ├── use-share │ │ ├── docs.stories.tsx │ │ └── index.tsx │ ├── use-themed-style.tsx │ └── use-theme-mode.tsx └── types │ └── utilities.d.ts ├── .github ├── assets │ ├── thumbnail.png │ └── tech-logos.png └── workflows │ ├── eas-build.yml │ ├── eas-update.yml │ ├── eas-preview.yml │ └── code-quality.yml ├── .vscode ├── settings.example.json └── extensions.json ├── .prettierignore ├── .env.example ├── lefthook.yml ├── .rnstorybook ├── index.ts ├── main.ts └── storybook.requires.ts ├── .prettierrc.cjs ├── run-jiti.js ├── eas.json ├── metro.config.cjs ├── tsconfig.json └── .gitignore /src/app/storybook/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from '../../../.rnstorybook'; 2 | -------------------------------------------------------------------------------- /.github/assets/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/.github/assets/thumbnail.png -------------------------------------------------------------------------------- /src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/src/assets/images/icon.png -------------------------------------------------------------------------------- /.github/assets/tech-logos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/.github/assets/tech-logos.png -------------------------------------------------------------------------------- /src/assets/images/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/src/assets/images/logo-dark.png -------------------------------------------------------------------------------- /src/assets/images/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/src/assets/images/logo-light.png -------------------------------------------------------------------------------- /src/assets/images/splash-icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/src/assets/images/splash-icon-dark.png -------------------------------------------------------------------------------- /src/features/app-onboarding/mascot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/src/features/app-onboarding/mascot.png -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from '@/locales/en'; 2 | import fr from '@/locales/fr'; 3 | 4 | export default { en, fr } as const; 5 | -------------------------------------------------------------------------------- /src/assets/images/splash-icon-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/src/assets/images/splash-icon-light.png -------------------------------------------------------------------------------- /.vscode/settings.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.readonlyInclude": { 3 | "**/*.gen.ts": true, 4 | "**/generated/**/*": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/images/android-icon-foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/src/assets/images/android-icon-foreground.png -------------------------------------------------------------------------------- /src/assets/images/android-icon-monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/src/assets/images/android-icon-monochrome.png -------------------------------------------------------------------------------- /src/features/app-onboarding/layout-login-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BearStudio/start-ui-native/HEAD/src/features/app-onboarding/layout-login-image.jpg -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .expo 2 | .vscode 3 | node_modules 4 | package.json 5 | package-lock.json 6 | yarn.lock 7 | pnpm-lock.yaml 8 | *.md 9 | *.mdx 10 | .env* 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "expo.vscode-expo-tools", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(logged)/(tabs)/books.tsx: -------------------------------------------------------------------------------- 1 | import { ViewBooks } from '@/features/books/view-books'; 2 | 3 | export default function Books() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(logged)/(tabs)/home.tsx: -------------------------------------------------------------------------------- 1 | import { ViewHome } from '@/features/home/view-home'; 2 | 3 | export default function HomeScreen() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(public)/sign-in.tsx: -------------------------------------------------------------------------------- 1 | import { ViewSignIn } from '@/features/auth/view-sign-in'; 2 | 3 | export default function SignIn() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/keep-awake.tsx: -------------------------------------------------------------------------------- 1 | import { useKeepAwake } from 'expo-keep-awake'; 2 | 3 | export const KeepAwake = () => { 4 | useKeepAwake(); 5 | 6 | return <>; 7 | }; 8 | -------------------------------------------------------------------------------- /src/locales/en/books.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "title": "Title", 4 | "author": "Author", 5 | "genre": "Genre", 6 | "publisher": "Publisher" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/locales/fr/books.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "title": "Titre", 4 | "author": "Auteur", 5 | "genre": "Genre", 6 | "publisher": "Éditeur" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/(logged)/(tabs)/account.tsx: -------------------------------------------------------------------------------- 1 | import { ViewAccount } from '@/features/account/view-account'; 2 | 3 | export default function Account() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(public)/otp-verification.tsx: -------------------------------------------------------------------------------- 1 | import { ViewOtpVerification } from '@/features/auth/view-otp-verification'; 2 | 3 | export default function () { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/ui/image.tsx: -------------------------------------------------------------------------------- 1 | import { Image as ExpoImage, ImageProps } from 'expo-image'; 2 | 3 | export const Image = (props: ImageProps) => { 4 | return ; 5 | }; 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_BASE_URL=https://demo.start-ui.com 2 | 3 | # OPTIONAL TO OVERRIDE 4 | # EXPO_PUBLIC_AUTH_URL=https://demo.start-ui.com/api/auth 5 | # EXPO_PUBLIC_OPENAPI_URL=https://demo.start-ui.com/api/openapi/app/schema -------------------------------------------------------------------------------- /src/lib/ficus-ui/foundations/font-sizes.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | '3xs': 8, 3 | '2xs': 10, 4 | xs: 12, 5 | sm: 14, 6 | md: 16, 7 | lg: 18, 8 | xl: 20, 9 | '2xl': 24, 10 | '3xl': 30, 11 | '4xl': 36, 12 | '5xl': 48, 13 | '6xl': 60, 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/ui/full-loader.tsx: -------------------------------------------------------------------------------- 1 | import { ActivityIndicator } from 'react-native'; 2 | import { Center } from 'react-native-ficus-ui'; 3 | 4 | export const FullLoader = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/(logged)/books/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useLocalSearchParams } from 'expo-router'; 2 | 3 | import { ViewBook } from '@/features/books/view-book'; 4 | 5 | export default function Book() { 6 | const { id } = useLocalSearchParams<{ id: string }>(); 7 | 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/foundations/fonts.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | fontFamily: {}, 3 | fontWeights: { 4 | hairline: 100, 5 | thin: 200, 6 | light: 300, 7 | normal: 400, 8 | medium: 500, 9 | semibold: 600, 10 | bold: 700, 11 | extrabold: 800, 12 | black: 900, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/constants/device.ts: -------------------------------------------------------------------------------- 1 | import * as Device from 'expo-device'; 2 | 3 | export const isApple = Device.brand === 'Apple'; 4 | 5 | export const isAndroid = Device.osName === 'Android'; 6 | 7 | export const isTablet = Device.deviceType === Device.DeviceType.TABLET; 8 | 9 | export const isPhone = Device.deviceType === Device.DeviceType.PHONE; 10 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | commands: 3 | check: 4 | glob: '*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}' 5 | run: pnpm prettier --write {staged_files} 6 | stage_fixed: true 7 | 8 | pre-push: 9 | commands: 10 | check: 11 | glob: '*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}' 12 | run: pnpm lint 13 | -------------------------------------------------------------------------------- /.rnstorybook/index.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | 3 | import { view } from './storybook.requires'; 4 | 5 | const StorybookUIRoot = view.getStorybookUI({ 6 | storage: { 7 | getItem: AsyncStorage.getItem, 8 | setItem: AsyncStorage.setItem, 9 | }, 10 | }); 11 | 12 | export default StorybookUIRoot; 13 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/theme.ts: -------------------------------------------------------------------------------- 1 | import appConfig from 'app.config'; 2 | 3 | import components from '@/lib/ficus-ui/components'; 4 | import foundations from '@/lib/ficus-ui/foundations'; 5 | 6 | export const STORAGE_KEY_THEME = `${appConfig.scheme}-theme`; 7 | 8 | export default { 9 | name: STORAGE_KEY_THEME, 10 | ...foundations, 11 | components, 12 | }; 13 | -------------------------------------------------------------------------------- /src/locales/en/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabs": { 3 | "home": { 4 | "title": "Home" 5 | }, 6 | "books": { 7 | "title": "Books" 8 | }, 9 | "account": { 10 | "title": "Account" 11 | } 12 | }, 13 | "notFound": { 14 | "title": "This page doesn't exists 🤔", 15 | "backInSafety": "Back in safe place" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/locales/fr/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabs": { 3 | "home": { 4 | "title": "Accueil" 5 | }, 6 | "books": { 7 | "title": "Livres" 8 | }, 9 | "account": { 10 | "title": "Compte" 11 | } 12 | }, 13 | "notFound": { 14 | "title": "Cette page n'existe pas 🤔", 15 | "backInSafety": "Retourner en lieu sûr" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/hey-api/api.ts: -------------------------------------------------------------------------------- 1 | import { client } from '@/lib/hey-api/generated/client.gen'; 2 | 3 | import { authClient } from '@/features/auth/client'; 4 | 5 | export * as api from './generated/@tanstack/react-query.gen'; 6 | 7 | client.interceptors.request.use((request) => { 8 | request.headers.append('Cookie', authClient.getCookie()); 9 | 10 | return request; 11 | }); 12 | -------------------------------------------------------------------------------- /src/locales/en/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": { 3 | "title": "Welcome to Start UI [native]", 4 | "description": "An opinionated native starter created & maintained by the BearStudio Team and other contributors. It represents our team's up-to-date stack that we use when creating native apps for our clients.", 5 | "openIssue": "Open Issue", 6 | "share": "Share" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/tanstack-form/components/form-field-helper.tsx: -------------------------------------------------------------------------------- 1 | import { Text, TextProps } from 'react-native-ficus-ui'; 2 | 3 | export const FormFieldHelper = (props: TextProps) => { 4 | return ( 5 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/components/divider.ts: -------------------------------------------------------------------------------- 1 | import { defineStyleConfig, Dict, DividerProps } from 'react-native-ficus-ui'; 2 | 3 | export default defineStyleConfig< 4 | DividerProps, 5 | Dict, 6 | Dict 7 | >({ 8 | baseStyle: { color: 'neutral.200', _dark: { color: 'neutral.800' } }, 9 | defaultProps: {}, 10 | variants: {}, 11 | sizes: {}, 12 | }); 13 | -------------------------------------------------------------------------------- /src/lib/tanstack-form/components/form.tsx: -------------------------------------------------------------------------------- 1 | import { AnyFormApi } from '@tanstack/react-form'; 2 | 3 | import { formContext as FormContext } from '@/lib/tanstack-form/context'; 4 | 5 | export const Form = ( 6 | props: React.PropsWithChildren<{ 7 | form: AnyFormApi; 8 | className?: string; 9 | }> 10 | ) => { 11 | return {props.children}; 12 | }; 13 | -------------------------------------------------------------------------------- /.rnstorybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-native'; 2 | 3 | const main: StorybookConfig = { 4 | stories: [ 5 | './stories/**/*.stories.?(ts|tsx|js|jsx)', 6 | '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)', 7 | ], 8 | addons: [ 9 | '@storybook/addon-ondevice-controls', 10 | '@storybook/addon-ondevice-actions', 11 | ], 12 | }; 13 | 14 | export default main; 15 | -------------------------------------------------------------------------------- /src/components/version.tsx: -------------------------------------------------------------------------------- 1 | import { nativeApplicationVersion, nativeBuildVersion } from 'expo-application'; 2 | import { Text, TextProps } from 'react-native-ficus-ui'; 3 | 4 | export const Version = (props: TextProps) => { 5 | return ( 6 | 7 | Version {nativeApplicationVersion} • {nativeBuildVersion} 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": { 3 | "label": "Language", 4 | "values": { 5 | "en": "English", 6 | "fr": "French", 7 | "ar": "Arabic", 8 | "sw": "Swahili" 9 | } 10 | }, 11 | "themes": { 12 | "label": "Theme", 13 | "values": { 14 | "system": "System (auto)", 15 | "light": "Light", 16 | "dark": "Dark" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/locales/fr/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": { 3 | "label": "Langue", 4 | "values": { 5 | "en": "Anglais", 6 | "fr": "Français", 7 | "ar": "Arabe", 8 | "sw": "Swahili" 9 | } 10 | }, 11 | "themes": { 12 | "values": { 13 | "light": "Clair", 14 | "dark": "Sombre", 15 | "system": "Système (auto)" 16 | }, 17 | "label": "Thème" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/locales/fr/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": { 3 | "title": "Bienvenue sur Start UI [native]", 4 | "description": "Un starter native \"opinionné\", conçu et maintenu par l’équipe BearStudio et d’autres contributeurs. Il reflète la stack actuelle que notre équipe privilégie pour développer des applications natives pour nos clients.", 5 | "openIssue": "Ouvrir un ticket", 6 | "share": "Partager" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/storybook/_layout.tsx: -------------------------------------------------------------------------------- 1 | import Constants from 'expo-constants'; 2 | import { Stack } from 'expo-router'; 3 | 4 | export default function StorybookLayout() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/layout/view-safe-content.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | import { ficus } from 'react-native-ficus-ui'; 3 | import { SafeAreaView } from 'react-native-safe-area-context'; 4 | 5 | const FicusSafeAreaView = ficus(SafeAreaView); 6 | 7 | export const ViewSafeContent = ( 8 | props: ComponentProps 9 | ) => { 10 | return ; 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/components/text.ts: -------------------------------------------------------------------------------- 1 | import { defineStyleConfig, Dict, TextProps } from 'react-native-ficus-ui'; 2 | 3 | export default defineStyleConfig, Dict>({ 4 | baseStyle: { color: 'neutral.900', _dark: { color: 'neutral.100' } }, 5 | variants: { 6 | muted: { 7 | color: 'neutral.600', 8 | _dark: { color: 'neutral.400' }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/foundations/colors.ts: -------------------------------------------------------------------------------- 1 | import tailwindColors from './tailwind-colors'; 2 | 3 | export default { 4 | // Update me with other Tailwind colors or with https://smart-swatch.netlify.app/ 5 | brand: tailwindColors.neutral, 6 | neutral: tailwindColors.neutral, 7 | positive: tailwindColors.green, 8 | negative: tailwindColors.red, 9 | warning: tailwindColors.amber, 10 | info: tailwindColors.sky, 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/hooks/use-browser/docs.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'react-native-ficus-ui'; 2 | 3 | import { useBrowser } from '@/hooks/use-browser'; 4 | 5 | export default { 6 | title: 'Hooks/useBrowser', 7 | }; 8 | 9 | export const Default = () => { 10 | const browser = useBrowser(); 11 | return ( 12 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/tanstack-form/components/form-field-label.tsx: -------------------------------------------------------------------------------- 1 | import { Text, TextProps } from 'react-native-ficus-ui'; 2 | 3 | type FormFieldLabelProps = TextProps; 4 | 5 | export const FormFieldLabel = (props: FormFieldLabelProps) => { 6 | return ( 7 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/components/index.ts: -------------------------------------------------------------------------------- 1 | import Badge from './badge'; 2 | import Button from './button'; 3 | import Divider from './divider'; 4 | import Input from './input'; 5 | import { pinInputFieldTheme, pinInputTheme } from './pin-input'; 6 | import Text from './text'; 7 | 8 | export default { 9 | Badge, 10 | Button, 11 | Divider, 12 | Input, 13 | Text, 14 | PinInput: pinInputTheme, 15 | PinInputField: pinInputFieldTheme, 16 | }; 17 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | // Prettier configuration. Please avoid changing the current configuration. 2 | // But if you do so, please run the `npm run pretty` command. 3 | /** @type {import("prettier").Options} */ 4 | const config = { 5 | endOfLine: 'lf', 6 | semi: true, 7 | singleQuote: true, 8 | tabWidth: 2, 9 | trailingComma: 'es5', 10 | arrowParens: 'always', 11 | }; 12 | 13 | // eslint-disable-next-line no-undef 14 | module.exports = config; 15 | -------------------------------------------------------------------------------- /run-jiti.js: -------------------------------------------------------------------------------- 1 | import { createJiti } from 'jiti'; 2 | import { fileURLToPath, URL } from 'node:url'; 3 | 4 | const moduleFileUrl = import.meta.url; 5 | 6 | // eslint-disable-next-line no-undef 7 | const pathToFile = process.argv[2]; 8 | 9 | // Allows aliases 10 | const jiti = createJiti(fileURLToPath(moduleFileUrl), { 11 | alias: { 12 | '@': fileURLToPath(new URL('./app', moduleFileUrl)), 13 | }, 14 | }); 15 | 16 | await jiti.import(pathToFile); 17 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | import { useColorScheme } from 'react-native'; 3 | import { Toaster } from 'sonner-native'; 4 | 5 | export const Sonner = ({ ...props }: ComponentProps) => { 6 | const scheme = useColorScheme(); 7 | return ( 8 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/foundations/index.ts: -------------------------------------------------------------------------------- 1 | import { Dict, Theme } from 'react-native-ficus-ui'; 2 | 3 | import colors from '@/lib/ficus-ui/foundations/colors'; 4 | import fontSizes from '@/lib/ficus-ui/foundations/font-sizes'; 5 | import fonts from '@/lib/ficus-ui/foundations/fonts'; 6 | 7 | export default { 8 | colors, 9 | fonts, 10 | fontSizes, 11 | } satisfies { 12 | colors: Dict; 13 | fonts: Theme['fonts']; 14 | fontSizes: Theme['fontSizes']; 15 | }; 16 | -------------------------------------------------------------------------------- /src/locales/en/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "signOut": "Sign out", 4 | "name": "Name", 5 | "email": "Email", 6 | "updateName": { 7 | "title": "Update name", 8 | "cancel": "Cancel", 9 | "save": "Save", 10 | "error": "An error occurred during name update" 11 | } 12 | }, 13 | "displayPreferences": { 14 | "title": "Display preferences", 15 | "theme": "Theme", 16 | "language": "Language" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/locales/fr/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "signOut": "Déconnexion", 4 | "name": "Nom", 5 | "email": "Email", 6 | "updateName": { 7 | "title": "Modifier le nom", 8 | "cancel": "Annuler", 9 | "save": "Sauvegarder", 10 | "error": "Une erreur est survenue lors de la modification du nom" 11 | } 12 | }, 13 | "displayPreferences": { 14 | "title": "Préférences d'affichage", 15 | "theme": "Thème", 16 | "language": "Langue" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/locales/en/app-onboarding.json: -------------------------------------------------------------------------------- 1 | { 2 | "continue": "Continue", 3 | "end": "Let's start", 4 | "welcome": { 5 | "title": "Welcome on" 6 | }, 7 | "features": { 8 | "titleOne": "Opinionated", 9 | "titleTwo": "UI Starter", 10 | "authentication": "Authentication", 11 | "account": "Account", 12 | "darkMode": "Dark mode", 13 | "translations": "Translations", 14 | "storybook": "Storybook", 15 | "devtools": "Devtools", 16 | "more": "And more..." 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/use-share/docs.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'react-native-ficus-ui'; 2 | 3 | import { useShare } from '@/hooks/use-share'; 4 | 5 | export default { 6 | title: 'Hooks/useShare', 7 | }; 8 | 9 | export const Default = () => { 10 | const share = useShare(); 11 | return ( 12 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/fr/app-onboarding.json: -------------------------------------------------------------------------------- 1 | { 2 | "continue": "Continuer", 3 | "end": "Commencer", 4 | "welcome": { 5 | "title": "Bienvenue sur" 6 | }, 7 | "features": { 8 | "titleOne": "\"Opinionated\"", 9 | "titleTwo": "UI Starter", 10 | "authentication": "Authentification", 11 | "account": "Compte", 12 | "darkMode": "Mode sombre", 13 | "translations": "Traductions", 14 | "storybook": "Storybook", 15 | "devtools": "Devtools", 16 | "more": "Et plus..." 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/hey-api/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@hey-api/openapi-ts'; 2 | 3 | const openapiUrl: string = 4 | process.env.EXPO_PUBLIC_OPENAPI_URL ?? 5 | `${process.env.EXPO_PUBLIC_BASE_URL}/api/openapi/app/schema`; 6 | 7 | export default defineConfig({ 8 | input: { path: openapiUrl }, 9 | output: { path: 'src/lib/hey-api/generated' }, 10 | plugins: [ 11 | { 12 | name: '@tanstack/react-query', 13 | }, 14 | { 15 | name: '@hey-api/typescript', 16 | }, 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/icon.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react-native'; 2 | import { ficus, StyleProps } from 'react-native-ficus-ui'; 3 | 4 | export const Icon = ({ 5 | icon, 6 | size, 7 | ...props 8 | }: StyleProps & { 9 | icon: LucideIcon | ReturnType; 10 | color?: string; 11 | size?: number; 12 | }) => { 13 | const FicusLucideIcon = ficus(icon); 14 | return ( 15 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 16.20.0", 4 | "appVersionSource": "remote" 5 | }, 6 | "build": { 7 | "development": { 8 | "developmentClient": true, 9 | "distribution": "internal", 10 | "channel": "development" 11 | }, 12 | "preview": { 13 | "distribution": "internal", 14 | "channel": "preview" 15 | }, 16 | "production": { 17 | "autoIncrement": true, 18 | "channel": "production" 19 | } 20 | }, 21 | "submit": { 22 | "production": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /metro.config.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable @typescript-eslint/no-require-imports */ 3 | const path = require('path'); 4 | const { getDefaultConfig } = require('expo/metro-config'); 5 | 6 | const withStorybook = require('@storybook/react-native/metro/withStorybook'); 7 | 8 | /** @type {import('expo/metro-config').MetroConfig} */ 9 | const config = getDefaultConfig(__dirname); 10 | 11 | module.exports = withStorybook(config, { 12 | enabled: true, 13 | configPath: path.resolve(__dirname, './.rnstorybook'), 14 | }); 15 | -------------------------------------------------------------------------------- /src/hooks/use-themed-style.tsx: -------------------------------------------------------------------------------- 1 | import { useColorModeValue } from 'react-native-ficus-ui'; 2 | 3 | import theme from '@/lib/ficus-ui/theme'; 4 | 5 | export const useThemedStyle = () => { 6 | return useColorModeValue( 7 | { 8 | backgroundColor: 'white', 9 | color: theme.colors.neutral[950], 10 | sceneBackgroundColor: theme.colors.neutral[50], 11 | }, 12 | { 13 | backgroundColor: theme.colors.neutral[950], 14 | color: 'white', 15 | sceneBackgroundColor: theme.colors.neutral[900], 16 | } 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/haptic-tab.tsx: -------------------------------------------------------------------------------- 1 | import { PlatformPressable } from '@react-navigation/elements'; 2 | import * as Haptics from 'expo-haptics'; 3 | import { ComponentProps } from 'react'; 4 | 5 | export const HapticTab = (props: ComponentProps) => { 6 | return ( 7 | { 10 | if (process.env.EXPO_OS === 'ios') { 11 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); 12 | } 13 | props.onPressIn?.(ev); 14 | }} 15 | /> 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/tanstack-form/components/index.ts: -------------------------------------------------------------------------------- 1 | import { Form as _Form } from './form'; 2 | import { FormField as _FormField } from './form-field'; 3 | import { FormFieldError as _FormFieldError } from './form-field-error'; 4 | import { FormFieldHelper as _FormFieldHelper } from './form-field-helper'; 5 | import { FormFieldLabel as _FormFieldLabel } from './form-field-label'; 6 | 7 | export const Form = _Form; 8 | export const FormField = _FormField; 9 | export const FormFieldError = _FormFieldError; 10 | export const FormFieldHelper = _FormFieldHelper; 11 | export const FormFieldLabel = _FormFieldLabel; 12 | -------------------------------------------------------------------------------- /src/features/app-onboarding/store.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { create } from 'zustand'; 3 | import { createJSONStorage, persist } from 'zustand/middleware'; 4 | 5 | interface OnboardingStore { 6 | done: boolean; 7 | setDone: () => void; 8 | } 9 | 10 | export const useOnboardingStore = create()( 11 | persist( 12 | (set) => ({ 13 | done: false, 14 | setDone: () => set({ done: true }), 15 | }), 16 | { 17 | name: 'onboarding', 18 | storage: createJSONStorage(() => AsyncStorage), 19 | } 20 | ) 21 | ); 22 | -------------------------------------------------------------------------------- /src/features/devtools/devtools.tsx: -------------------------------------------------------------------------------- 1 | import { useAsyncStorageDevTools } from '@dev-plugins/async-storage'; 2 | import { useReactNavigationDevTools } from '@dev-plugins/react-navigation'; 3 | import { useReactQueryDevTools } from '@dev-plugins/react-query'; 4 | import { useNavigationContainerRef } from 'expo-router'; 5 | 6 | import { queryClient } from '@/app/_layout'; 7 | 8 | export const DevTools = () => { 9 | useReactQueryDevTools(queryClient); 10 | 11 | const navigationRef = useNavigationContainerRef(); 12 | useReactNavigationDevTools(navigationRef); 13 | 14 | useAsyncStorageDevTools(); 15 | 16 | return <>; 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-credit-card.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/locales/en/index.ts: -------------------------------------------------------------------------------- 1 | import 'dayjs/locale/en.js'; 2 | 3 | import account from './account.json' with { type: 'json' }; 4 | import appOnboarding from './app-onboarding.json' with { type: 'json' }; 5 | import auth from './auth.json' with { type: 'json' }; 6 | import books from './books.json' with { type: 'json' }; 7 | import common from './common.json' with { type: 'json' }; 8 | import home from './home.json' with { type: 'json' }; 9 | import layout from './layout.json' with { type: 'json' }; 10 | 11 | export default { 12 | account, 13 | books, 14 | auth, 15 | common, 16 | home, 17 | layout, 18 | appOnboarding, 19 | } as const; 20 | -------------------------------------------------------------------------------- /src/locales/fr/index.ts: -------------------------------------------------------------------------------- 1 | import 'dayjs/locale/fr.js'; 2 | 3 | import account from './account.json' with { type: 'json' }; 4 | import appOnboarding from './app-onboarding.json' with { type: 'json' }; 5 | import auth from './auth.json' with { type: 'json' }; 6 | import books from './books.json' with { type: 'json' }; 7 | import common from './common.json' with { type: 'json' }; 8 | import home from './home.json' with { type: 'json' }; 9 | import layout from './layout.json' with { type: 'json' }; 10 | 11 | export default { 12 | account, 13 | auth, 14 | books, 15 | common, 16 | home, 17 | layout, 18 | appOnboarding, 19 | } as const; 20 | -------------------------------------------------------------------------------- /src/hooks/use-theme-mode.tsx: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | import z from 'zod'; 4 | 5 | import { STORAGE_KEY_THEME } from '@/lib/ficus-ui/theme'; 6 | 7 | export const themeQueryKey = ['app-theme', STORAGE_KEY_THEME]; 8 | 9 | const zTheme = () => z.enum(['system', 'light', 'dark']).default('system'); 10 | 11 | export const useThemeMode = () => { 12 | return useQuery({ 13 | queryKey: themeQueryKey, 14 | queryFn: () => 15 | AsyncStorage.getItem(STORAGE_KEY_THEME).then((value) => 16 | zTheme().parse(value) 17 | ), 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/layout/splash-screen-manager.tsx: -------------------------------------------------------------------------------- 1 | import { SplashScreen } from 'expo-router'; 2 | import { PropsWithChildren, useEffect } from 'react'; 3 | 4 | import { authClient } from '@/features/auth/client'; 5 | 6 | export const SplashScreenManager = (props: PropsWithChildren) => { 7 | const session = authClient.useSession(); 8 | 9 | const isAppReady = !session.isPending; 10 | 11 | // Manage splashscreen hide when all wanted content is loaded 12 | // For example session, fonts, required queries etc 13 | useEffect(() => { 14 | if (isAppReady) { 15 | SplashScreen.hide(); 16 | } 17 | }, [isAppReady]); 18 | 19 | return props.children; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-more-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-dashboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-check-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "moduleResolution": "Bundler", 6 | "module": "ESNext", 7 | "target": "ES2022", 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "strictNullChecks": true, 11 | "noUncheckedIndexedAccess": true, 12 | "noImplicitAny": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | "typeRoots": ["node_modules/@types", "src/types"], 18 | "plugins": [ 19 | { 20 | "name": "ts-plugin-sort-import-suggestions", 21 | "moveUpPatterns": ["@/", "\\.{1,2}/"] 22 | } 23 | ] 24 | }, 25 | "include": ["src", ".expo/types/**/*.ts", "expo-env.d.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /src/layout/view-tab-content.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps, ScrollBox } from 'react-native-ficus-ui'; 2 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 3 | 4 | import { WITH_NATIVE_TABS } from '@/app/(logged)/(tabs)/_layout'; 5 | import { isApple } from '@/constants/device'; 6 | 7 | export const ViewTabContent = ({ 8 | withHeader = isApple && WITH_NATIVE_TABS, 9 | children, 10 | ...props 11 | }: BoxProps & { withHeader?: boolean }) => { 12 | const insets = useSafeAreaInsets(); 13 | 14 | return ( 15 | 21 | {children} 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-chevrons-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-chevrons-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/i18n/constants.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | import locales from '@/locales'; 4 | 5 | export type Language = { 6 | key: keyof typeof locales; 7 | dir?: 'ltr' | 'rtl'; 8 | fontScale?: number; 9 | }; 10 | 11 | export const DEFAULT_NAMESPACE = 'common'; 12 | 13 | export const DEFAULT_LANGUAGE_KEY: Language['key'] = 'en'; 14 | 15 | export type LanguageKey = (typeof AVAILABLE_LANGUAGES)[number]['key']; 16 | export const AVAILABLE_LANGUAGES = [ 17 | { 18 | key: 'en', 19 | } as const, 20 | { 21 | key: 'fr', 22 | } as const, 23 | ] satisfies Language[]; 24 | 25 | export const zAvailableLanguages = () => 26 | z 27 | .enum(AVAILABLE_LANGUAGES.map((lang) => lang.key)) 28 | .default(DEFAULT_LANGUAGE_KEY); 29 | 30 | export const LANGUAGE_STORAGE_KEY = 'language'; 31 | -------------------------------------------------------------------------------- /src/features/app-onboarding/use-background-animated-style.tsx: -------------------------------------------------------------------------------- 1 | import { WINDOW_WIDTH } from '@gorhom/bottom-sheet'; 2 | import { 3 | interpolate, 4 | SharedValue, 5 | useAnimatedStyle, 6 | } from 'react-native-reanimated'; 7 | 8 | import { appOnboardingScreens } from '@/features/app-onboarding/view-app-onboarding'; 9 | 10 | const backgroundImageSize = { width: 1536, height: 1024 }; 11 | 12 | export const useBackgroundAnimatedStyle = (scrollX: SharedValue) => { 13 | return useAnimatedStyle(() => { 14 | const maxScrollX = WINDOW_WIDTH * (appOnboardingScreens.length - 1); 15 | 16 | const right = interpolate( 17 | scrollX.value, 18 | [0, maxScrollX], 19 | [650, Math.min(750, backgroundImageSize.width - WINDOW_WIDTH)] 20 | ); 21 | 22 | return { right }; 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-alert-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/docs.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import { Button, ScrollBox, Stack } from 'react-native-ficus-ui'; 3 | 4 | import * as icons from './generated'; 5 | 6 | export default { 7 | title: 'Icons/Custom', 8 | }; 9 | 10 | export const AllIcons = () => ( 11 | 12 | 13 | {Object.entries(icons).map(([name, Icon]) => ( 14 | 15 | 16 | 17 | ))} 18 | 19 | 20 | ); 21 | 22 | const CustomIcon = ({ 23 | children, 24 | name, 25 | }: { 26 | children: ReactElement; 27 | name: string; 28 | }) => { 29 | return ( 30 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | BoxProps, 4 | HStack, 5 | StackProps, 6 | Text, 7 | TextProps, 8 | } from 'react-native-ficus-ui'; 9 | 10 | export const Card = (props: BoxProps) => { 11 | return ( 12 | 20 | ); 21 | }; 22 | 23 | export const CardHeader = (props: StackProps) => { 24 | return ; 25 | }; 26 | 27 | export const CardTitle = (props: TextProps) => { 28 | return ; 29 | }; 30 | 31 | export const CardBody = (props: BoxProps) => { 32 | return ; 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-chevrons-up-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/components/input.ts: -------------------------------------------------------------------------------- 1 | import { defineStyleConfig, Dict, InputProps } from 'react-native-ficus-ui'; 2 | 3 | export default defineStyleConfig< 4 | InputProps, 5 | Dict, 6 | Dict 7 | >({ 8 | defaultProps: { variant: 'default', size: 'lg' }, 9 | baseStyle: { 10 | borderRadius: 'md', 11 | _disabled: { opacity: 0.7 }, 12 | _focused: { borderWidth: 2 }, 13 | }, 14 | sizes: { sm: { h: 32 }, md: { h: 36 }, lg: { h: 40 } }, 15 | variants: { 16 | default: { 17 | borderWidth: 1, 18 | borderColor: 'neutral.300', 19 | placeholderTextColor: 'neutral.600', 20 | backgroundColor: 'white', 21 | _dark: { 22 | borderColor: 'neutral.600', 23 | backgroundColor: 'neutral.950', 24 | placeholderTextColor: 'neutral.300', 25 | }, 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/lib/i18n/config.ts: -------------------------------------------------------------------------------- 1 | import { InitOptions } from 'i18next'; 2 | 3 | import { DEFAULT_LANGUAGE_KEY, DEFAULT_NAMESPACE } from '@/lib/i18n/constants'; 4 | 5 | import locales from '@/locales'; 6 | 7 | export const i18nConfig = ( 8 | language: keyof typeof locales | undefined 9 | ): InitOptions => ({ 10 | compatibilityJSON: 'v4', 11 | defaultNS: DEFAULT_NAMESPACE, 12 | ns: Object.keys(locales[DEFAULT_LANGUAGE_KEY]), 13 | lng: language, 14 | resources: locales, 15 | fallbackLng: DEFAULT_LANGUAGE_KEY, 16 | supportedLngs: Object.keys(locales), 17 | 18 | // Fix issue with i18next types 19 | // https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz 20 | returnNull: false, 21 | 22 | interpolation: { 23 | escapeValue: false, // react already safes from xss 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/ui/animated-step-indicator.tsx: -------------------------------------------------------------------------------- 1 | import { ViewProps, ViewStyle } from 'react-native'; 2 | import Animated from 'react-native-reanimated'; 3 | 4 | type AnimatedStepIndicatorProps = { isActive?: boolean }; 5 | 6 | export const AnimatedStepIndicator = ( 7 | props: AnimatedStepIndicatorProps & 8 | Omit & { style?: ViewStyle } 9 | ) => { 10 | return ( 11 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconPanel.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconPanel = ficus(_Svg); 21 | export default IconPanel; 22 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-house-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/features/books/book-cover.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps, Text } from 'react-native-ficus-ui'; 2 | 3 | import { BookGetByIdResponse } from '@/lib/hey-api/generated'; 4 | 5 | export type BookCoverProps = BoxProps & { book: BookGetByIdResponse }; 6 | 7 | const COVER_HEIGHT = 240; 8 | 9 | export const BookCover = ({ 10 | book, 11 | h = COVER_HEIGHT, 12 | ...props 13 | }: BookCoverProps) => { 14 | return ( 15 | 25 | 26 | {book.title} 27 | 28 | 29 | {book.author} 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/features/auth/auth-header.tsx: -------------------------------------------------------------------------------- 1 | import { HStack } from 'react-native-ficus-ui'; 2 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 3 | 4 | import { Logo } from '@/components/icons/generated'; 5 | import { LocaleSwitcher } from '@/components/ui/locale-switcher'; 6 | import { ThemeSwitcher } from '@/components/ui/theme-switcher'; 7 | 8 | export const AuthHeader = () => { 9 | const insets = useSafeAreaInsets(); 10 | 11 | return ( 12 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/auth/client.ts: -------------------------------------------------------------------------------- 1 | import { expoClient } from '@better-auth/expo/client'; 2 | import appConfig from 'app.config'; 3 | import { 4 | emailOTPClient, 5 | inferAdditionalFields, 6 | } from 'better-auth/client/plugins'; 7 | import { createAuthClient } from 'better-auth/react'; 8 | import * as SecureStore from 'expo-secure-store'; 9 | 10 | const authBaseURL: string = 11 | process.env.EXPO_PUBLIC_AUTH_URL ?? 12 | `${process.env.EXPO_PUBLIC_BASE_URL}/api/auth`; 13 | 14 | export const authClient = createAuthClient({ 15 | baseURL: authBaseURL, 16 | plugins: [ 17 | expoClient({ 18 | scheme: appConfig.scheme, 19 | storagePrefix: appConfig.scheme, 20 | storage: SecureStore, 21 | }), 22 | emailOTPClient(), 23 | inferAdditionalFields({ 24 | user: { 25 | onboardedAt: { 26 | type: 'date', 27 | }, 28 | }, 29 | }), 30 | ], 31 | }); 32 | -------------------------------------------------------------------------------- /.github/workflows/eas-build.yml: -------------------------------------------------------------------------------- 1 | name: 🔨 EAS build 2 | 3 | env: 4 | EXPO_PUBLIC_API_URL: ${{ vars.API_URL }} 5 | EXPO_PUBLIC_AUTH_URL: ${{ vars.AUTH_URL }} 6 | EXPO_PUBLIC_OPENAPI_URL: ${{ vars.OPENAPI_URL }} 7 | 8 | on: 9 | push: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: 🏗 Setup repo 17 | uses: actions/checkout@v5 18 | 19 | - name: 🏗 Setup Node 20 | uses: actions/setup-node@v5 21 | with: 22 | node-version: 'lts/*' 23 | cache: 'pnpm' 24 | 25 | - name: 🏗 Setup EAS 26 | uses: expo/expo-github-action@v8 27 | with: 28 | eas-version: latest 29 | token: ${{ secrets.EXPO_TOKEN }} 30 | 31 | - name: 📦 Install dependencies 32 | run: pnpm install 33 | 34 | - name: 🚀 Build app 35 | run: eas build --non-interactive 36 | -------------------------------------------------------------------------------- /src/components/theme-manager.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'expo-status-bar'; 2 | import { useEffect } from 'react'; 3 | import { useColorScheme } from 'react-native'; 4 | import { useColorMode } from 'react-native-ficus-ui'; 5 | 6 | import { useThemeMode } from '@/hooks/use-theme-mode'; 7 | 8 | export const ThemeManager = () => { 9 | const colorScheme = useColorScheme(); 10 | const ficusColorMode = useColorMode(); 11 | 12 | const setFicusColorMode = ficusColorMode.setColorMode; 13 | 14 | const themeQuery = useThemeMode(); 15 | const currentTheme = themeQuery.data ?? 'system'; 16 | 17 | useEffect(() => { 18 | if (colorScheme) { 19 | setFicusColorMode(currentTheme === 'system' ? colorScheme : currentTheme); 20 | } 21 | }, [currentTheme, colorScheme, setFicusColorMode]); 22 | 23 | return ( 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link, usePathname } from 'expo-router'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { Box, Button, Center, Text } from 'react-native-ficus-ui'; 4 | 5 | import { ViewSafeContent } from '@/layout/view-safe-content'; 6 | 7 | export default function NotFound() { 8 | const { t } = useTranslation(['layout']); 9 | 10 | const path = usePathname(); 11 | 12 | return ( 13 | 14 |
15 | {t('layout:notFound.title')} 16 | 17 | 18 | 19 | 20 | 21 | {process.env.NODE_ENV === 'development' && ( 22 | {path} 23 | )} 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/eas-update.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 EAS update 2 | 3 | env: 4 | EXPO_PUBLIC_API_URL: ${{ vars.API_URL }} 5 | EXPO_PUBLIC_AUTH_URL: ${{ vars.AUTH_URL }} 6 | EXPO_PUBLIC_OPENAPI_URL: ${{ vars.OPENAPI_URL }} 7 | 8 | on: 9 | push: 10 | branches: [main] 11 | 12 | jobs: 13 | update: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: 🏗 Setup repo 17 | uses: actions/checkout@v5 18 | 19 | - name: 🏗 Setup Node 20 | uses: actions/setup-node@v5 21 | with: 22 | node-version: 'lts/*' 23 | cache: 'pnpm' 24 | 25 | - name: 🏗 Setup EAS 26 | uses: expo/expo-github-action@v8 27 | with: 28 | eas-version: latest 29 | token: ${{ secrets.EXPO_TOKEN }} 30 | 31 | - name: 📦 Install dependencies 32 | run: pnpm install 33 | 34 | - name: 🚀 Create update 35 | run: eas update --auto --non-interactive 36 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # Expo 12 | .expo/ 13 | dist/ 14 | web-build/ 15 | expo-env.d.ts 16 | 17 | # Native 18 | .kotlin/ 19 | *.orig.* 20 | *.jks 21 | *.p8 22 | *.p12 23 | *.key 24 | *.mobileprovision 25 | 26 | # Metro 27 | .metro-health-check* 28 | 29 | # debug 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | 34 | # misc 35 | .DS_Store 36 | *.pem 37 | /.vscode/* 38 | !/.vscode/extensions.json 39 | !/.vscode/settings.example.json 40 | .idea/ 41 | .eslintcache 42 | .db 43 | tsconfig.tsbuildinfo 44 | 45 | # local env files 46 | .env 47 | .env.local 48 | .env.development.local 49 | .env.test.local 50 | .env.production.local 51 | 52 | # local builds 53 | android/ 54 | ios/ 55 | 56 | # hey-api 57 | src/lib/hey-api/generated/* 58 | openapi-ts-error-*.log 59 | 60 | .tanstack -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconCheck.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconCheck = ficus(_Svg); 23 | export default IconCheck; 24 | -------------------------------------------------------------------------------- /src/lib/tanstack-form/components/form-submit.tsx: -------------------------------------------------------------------------------- 1 | import { ActivityIndicator } from 'react-native'; 2 | import { Button, ButtonProps } from 'react-native-ficus-ui'; 3 | 4 | import { useFormContext } from '@/lib/tanstack-form/context'; 5 | 6 | export default function FormSubmit({ 7 | submitting, 8 | children, 9 | ...props 10 | }: Readonly< 11 | ButtonProps & { 12 | submitting?: React.ReactElement; 13 | children: React.ReactNode; 14 | } 15 | >) { 16 | const form = useFormContext(); 17 | return ( 18 | [state.canSubmit, state.isSubmitting]}> 19 | {([canSubmit, isSubmitting]) => ( 20 | 28 | )} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconChevronUp.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconChevronUp = ficus(_Svg); 23 | export default IconChevronUp; 24 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconPlay.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconPlay = ficus(_Svg); 23 | export default IconPlay; 24 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconChevronDown.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconChevronDown = ficus(_Svg); 23 | export default IconChevronDown; 24 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconChevronLeft.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconChevronLeft = ficus(_Svg); 23 | export default IconChevronLeft; 24 | -------------------------------------------------------------------------------- /src/lib/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import dayjs from 'dayjs'; 3 | import { getLocales } from 'expo-localization'; 4 | import i18n from 'i18next'; 5 | import { initReactI18next } from 'react-i18next'; 6 | 7 | import { i18nConfig } from '@/lib/i18n/config'; 8 | import { 9 | LANGUAGE_STORAGE_KEY, 10 | zAvailableLanguages, 11 | } from '@/lib/i18n/constants'; 12 | 13 | const initI18n = async () => { 14 | const language = await AsyncStorage.getItem(LANGUAGE_STORAGE_KEY); 15 | 16 | i18n 17 | .use(initReactI18next) 18 | .init( 19 | i18nConfig( 20 | zAvailableLanguages().parse(language ?? getLocales()[0]?.languageCode) 21 | ) 22 | ); 23 | }; 24 | 25 | initI18n(); 26 | 27 | export const syncLanguage = (langKey: string) => { 28 | dayjs.locale(langKey); 29 | AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, langKey); 30 | }; 31 | 32 | i18n.on('languageChanged', (langKey) => syncLanguage(langKey)); 33 | 34 | export default i18n; 35 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconChevronRight.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconChevronRight = ficus(_Svg); 23 | export default IconChevronRight; 24 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-repository.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-x-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconPlus.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconPlus = ficus(_Svg); 29 | export default IconPlus; 30 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconArrowDown.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconArrowDown = ficus(_Svg); 23 | export default IconArrowDown; 24 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconArrowUp.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconArrowUp = ficus(_Svg); 23 | export default IconArrowUp; 24 | -------------------------------------------------------------------------------- /src/lib/tanstack-form/context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFormHookContexts, 3 | ReactFormExtendedApi, 4 | } from '@tanstack/react-form'; 5 | 6 | const { 7 | fieldContext, 8 | useFieldContext, 9 | formContext, 10 | useFormContext: useTanstackFormContext, 11 | } = createFormHookContexts(); 12 | 13 | const useFormContext = >() => { 14 | const form = useTanstackFormContext(); 15 | 16 | return form as unknown as ReactFormExtendedApi< 17 | Fields, 18 | undefined, 19 | undefined, 20 | undefined, 21 | undefined, 22 | undefined, 23 | undefined, 24 | undefined, 25 | undefined, 26 | undefined, 27 | undefined, 28 | undefined 29 | >; 30 | }; 31 | 32 | // type WithForm = < 33 | // Fields, 34 | // undefined, 35 | // undefined, 36 | // undefined, 37 | // undefined, 38 | // undefined, 39 | // undefined, 40 | // undefined, 41 | // undefined, 42 | // undefined, 43 | // Props 44 | // > 45 | 46 | export { fieldContext, formContext, useFieldContext, useFormContext }; 47 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-house.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-circle-user-round.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-book-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/form/docs.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollBox, Stack } from 'react-native-ficus-ui'; 2 | 3 | import { useAppForm } from '@/lib/tanstack-form/config'; 4 | 5 | export default { 6 | title: 'Form/Fields', 7 | }; 8 | 9 | export const AllFields = () => { 10 | const form = useAppForm({ defaultValues: { text: '', otp: '' } }); 11 | return ( 12 | 13 | 14 | 15 | 16 | {(field) => ( 17 | 18 | FieldText 19 | 20 | 21 | )} 22 | 23 | 24 | {(field) => ( 25 | 26 | FieldOtp 27 | 28 | 29 | )} 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/tanstack-form/components/form-field.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useId } from 'react'; 2 | import { Box } from 'react-native-ficus-ui'; 3 | 4 | import { useFieldContext } from '@/lib/tanstack-form/context'; 5 | 6 | type FormFieldSize = 'sm' | 'default' | 'lg'; 7 | 8 | export type FieldContextMeta = ReturnType< 9 | ReturnType['getMeta'] 10 | > & { 11 | id: string; 12 | descriptionId: string; 13 | errorId: string; 14 | size?: FormFieldSize; 15 | }; 16 | 17 | export const FormField = (props: { 18 | id?: string; 19 | size?: FormFieldSize; 20 | children?: ReactNode; 21 | }) => { 22 | const _id = useId(); 23 | const id = props.id ?? _id; 24 | 25 | const field = useFieldContext(); 26 | 27 | const setFieldMeta = field.setMeta; 28 | 29 | useEffect(() => { 30 | setFieldMeta((meta) => ({ 31 | ...meta, 32 | id, 33 | descriptionId: `${id}-description`, 34 | errorId: `${id}-error`, 35 | size: props.size, 36 | })); 37 | }, [setFieldMeta, id, props.size]); 38 | 39 | return {props.children}; 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-hash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-trash-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-devices.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/locales/en/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "signin": { 3 | "title": "Login to your account", 4 | "subtitle": "Enter your email to login to your account", 5 | "email": { "placeholder": "Email" }, 6 | "loginWithEmail": "Login with email", 7 | "or": "or", 8 | "loginWithGithub": "Login with GitHub" 9 | }, 10 | "verification": { 11 | "back": "Back", 12 | "title": "Verification", 13 | "description": "If you have an account, we have sent a code to ", 14 | "enterItBelow": "Enter it below.", 15 | "verificationCode": { "label": "Verification code" }, 16 | "expireHint": "The code expires shortly", 17 | "confirm": "Confirm" 18 | }, 19 | "onboarding": { 20 | "title": "Welcome", 21 | "subtitle": "Let's personalize your experience", 22 | "name": { "label": "What is your name?" }, 23 | "continue": "Continue" 24 | }, 25 | "signOut": { 26 | "action": "Sign Out", 27 | "confirm": { 28 | "title": "Account Sign Out", 29 | "description": "You are about to end your session", 30 | "errorMessage": "Failed to Sign Out", 31 | "cancel": "Cancel", 32 | "signOut": "Sign out" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconCreditCard.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconCreditCard = ficus(_Svg); 29 | export default IconCreditCard; 30 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-log-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconDashboard.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconDashboard = ficus(_Svg); 21 | export default IconDashboard; 22 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconCheckCircle.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconCheckCircle = ficus(_Svg); 21 | export default IconCheckCircle; 22 | -------------------------------------------------------------------------------- /src/types/utilities.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/redundant-type-aliases */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | /** 4 | * Use this type to temporary bypass to `any` without writting `any` 5 | * Comment the line to find where it's used 6 | */ 7 | type TODO = any; 8 | 9 | /** 10 | * Use this type to use an explicit `any` 11 | */ 12 | type ExplicitAny = any; 13 | 14 | /** 15 | * Use this type to remove keys from T that are in U type. 16 | */ 17 | type RemoveFromType = Pick>; 18 | 19 | /** 20 | * Use this type to overwrite the keys of the first type with the second one. 21 | * This is mainly useful with custom props type that extends multiple components 22 | * with the `as` props. 23 | */ 24 | type Overwrite = RemoveFromType & U; 25 | 26 | type UnionKeys = T extends T ? keyof T : never; 27 | type StrictUnionHelper = T extends ExplicitAny 28 | ? T & Partial, keyof T>, undefined>> 29 | : never; 30 | type StrictUnion = StrictUnionHelper; 31 | 32 | /** 33 | * Clean up type for better DX 34 | */ 35 | type Prettify = { 36 | [K in keyof T]: T[K]; 37 | } & {}; 38 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-log-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-unlock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/features/app-onboarding/use-mascot-animated-style.tsx: -------------------------------------------------------------------------------- 1 | import { WINDOW_HEIGHT, WINDOW_WIDTH } from '@gorhom/bottom-sheet'; 2 | import { 3 | interpolate, 4 | SharedValue, 5 | useAnimatedStyle, 6 | } from 'react-native-reanimated'; 7 | 8 | import { appOnboardingScreens } from '@/features/app-onboarding/view-app-onboarding'; 9 | 10 | export const useMascotAnimatedStyle = (scrollX: SharedValue) => { 11 | return useAnimatedStyle(() => { 12 | const maxScrollX = WINDOW_WIDTH * (appOnboardingScreens.length - 1); 13 | 14 | const leftStart = WINDOW_WIDTH / 3.5; 15 | const topStart = WINDOW_HEIGHT / 2.5; 16 | 17 | const left = interpolate( 18 | scrollX.value, 19 | [0, maxScrollX], 20 | [leftStart, leftStart - 50] 21 | ); 22 | 23 | const top = interpolate( 24 | scrollX.value, 25 | [0, maxScrollX], 26 | [topStart, topStart + 80] 27 | ); 28 | 29 | const rotate = interpolate(scrollX.value, [0, maxScrollX], [2, 10]); 30 | 31 | const scale = interpolate(scrollX.value, [0, maxScrollX], [1, 0.7]); 32 | 33 | return { 34 | left, 35 | top, 36 | transform: [{ rotate: `${rotate}deg` }, { scale }], 37 | }; 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/hooks/use-share/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MutateOptions, 3 | useMutation, 4 | UseMutationOptions, 5 | } from '@tanstack/react-query'; 6 | import { Share, ShareAction, ShareContent, ShareOptions } from 'react-native'; 7 | import { toast } from 'sonner-native'; 8 | 9 | type UseShareMutationParameters = { 10 | content: ShareContent; 11 | options?: ShareOptions; 12 | }; 13 | 14 | export const useShare = ( 15 | mutationOptions?: UseMutationOptions< 16 | ShareAction, 17 | Error, 18 | UseShareMutationParameters 19 | > 20 | ) => { 21 | const mutation = useMutation({ 22 | ...mutationOptions, 23 | mutationFn: async ({ options, content }) => { 24 | return Share.share(content, options); 25 | }, 26 | onError: (error, ...params) => { 27 | toast.error(error.name); 28 | mutationOptions?.onError?.(error, ...params); 29 | }, 30 | }); 31 | 32 | return { 33 | ...mutation, 34 | open: ( 35 | content: ShareContent, 36 | options?: ShareOptions, 37 | mutationOptions?: MutateOptions< 38 | ShareAction, 39 | Error, 40 | UseShareMutationParameters 41 | > 42 | ) => mutation.mutate({ content, options }, mutationOptions), 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/lib/tanstack-form/components/form-field-error.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from '@tanstack/react-form'; 2 | import { HStack, Text, TextProps } from 'react-native-ficus-ui'; 3 | 4 | import { FieldContextMeta } from '@/lib/tanstack-form/components/form-field'; 5 | import { useFieldContext } from '@/lib/tanstack-form/context'; 6 | 7 | import { IconAlertCircle } from '@/components/icons/generated'; 8 | 9 | export const FormFieldError = (props: TextProps) => { 10 | const field = useFieldContext(); 11 | 12 | const meta = useStore(field.store, (state) => { 13 | const fieldMeta = state.meta as FieldContextMeta; 14 | return { 15 | errorMessage: fieldMeta.errors[0]?.message, 16 | errorId: fieldMeta.errorId, 17 | }; 18 | }); 19 | 20 | if (!meta.errorMessage) { 21 | return null; 22 | } 23 | 24 | return ( 25 | 26 | 27 | 35 | {meta.errorMessage} 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconChevronsLeft.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconChevronsLeft = ficus(_Svg); 21 | export default IconChevronsLeft; 22 | -------------------------------------------------------------------------------- /src/app/(public)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, useRouter } from 'expo-router'; 2 | import { useEffect } from 'react'; 3 | 4 | import { useThemedStyle } from '@/hooks/use-themed-style'; 5 | 6 | import { authClient } from '@/features/auth/client'; 7 | 8 | export default function PublicLayout() { 9 | const router = useRouter(); 10 | const session = authClient.useSession(); 11 | 12 | const themedStyle = useThemedStyle(); 13 | 14 | useEffect(() => { 15 | if (!session.isPending && session.data?.user) { 16 | router.replace('/(logged)/(tabs)/home'); 17 | } 18 | }, [router, session.data?.user, session.isPending]); 19 | 20 | return ( 21 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconChevronsRight.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconChevronsRight = ficus(_Svg); 21 | export default IconChevronsRight; 22 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconArrowLeft.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconArrowLeft = ficus(_Svg); 29 | export default IconArrowLeft; 30 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconArrowRight.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconArrowRight = ficus(_Svg); 29 | export default IconArrowRight; 30 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconClock.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconClock = ficus(_Svg); 29 | export default IconClock; 30 | -------------------------------------------------------------------------------- /src/locales/fr/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "signin": { 3 | "title": "Connexion à votre compte", 4 | "subtitle": "Entrez votre email ci-dessous pour vous connecter à votre compte", 5 | "email": { "placeholder": "Email" }, 6 | "loginWithEmail": "Connexion avec email", 7 | "or": "ou", 8 | "loginWithGithub": "Connexion avec GitHub" 9 | }, 10 | "verification": { 11 | "back": "Retour", 12 | "title": "Vérification", 13 | "description": "Si vous avez un compte, nous avons envoyé un code à ", 14 | "enterItBelow": "Entrez-le ci-dessous.", 15 | "verificationCode": { "label": "Code de vérification" }, 16 | "expireHint": "Le code expire bientôt", 17 | "confirm": "Confirmer" 18 | }, 19 | "onboarding": { 20 | "title": "Bienvenue", 21 | "subtitle": "Personnalisons votre expérience", 22 | "name": { "label": "Quel est votre nom ?" }, 23 | "continue": "Continuer" 24 | }, 25 | "signOut": { 26 | "action": "Déconnexion", 27 | "confirm": { 28 | "description": "Vous êtes sur le point de mettre fin à votre session", 29 | "errorMessage": "Échec de la déconnexion", 30 | "title": "Déconnexion du compte", 31 | "cancel": "Annuler", 32 | "signOut": "Déconnexion" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconSearch.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconSearch = ficus(_Svg); 29 | export default IconSearch; 30 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import Constants from 'expo-constants'; 2 | import { Redirect, router, useFocusEffect } from 'expo-router'; 3 | import { useCallback } from 'react'; 4 | 5 | import { FullLoader } from '@/components/ui/full-loader'; 6 | 7 | import { useOnboardingStore } from '@/features/app-onboarding/store'; 8 | import { ViewOnboarding } from '@/features/app-onboarding/view-app-onboarding'; 9 | import { authClient } from '@/features/auth/client'; 10 | 11 | export default function Index() { 12 | const session = authClient.useSession(); 13 | const isOnboarded = useOnboardingStore((state) => state.done); 14 | 15 | // Manage first app redirection 16 | useFocusEffect( 17 | useCallback(() => { 18 | if (!isOnboarded || session.isPending) { 19 | return; 20 | } 21 | router.replace( 22 | session.data?.user?.id ? '/(logged)/(tabs)/home' : '/(public)/sign-in' 23 | ); 24 | }, [session.data, session.isPending, isOnboarded]) 25 | ); 26 | 27 | if (!isOnboarded) { 28 | return ; 29 | } 30 | 31 | if (Constants.expoConfig?.extra?.isStorybook) { 32 | return ; 33 | } 34 | 35 | if (session.isPending) { 36 | return ; 37 | } 38 | 39 | return <>; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconX.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconX = ficus(_Svg); 29 | export default IconX; 30 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconMoreVertical.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 33 | ); 34 | const IconMoreVertical = ficus(_Svg); 35 | export default IconMoreVertical; 36 | -------------------------------------------------------------------------------- /.github/workflows/eas-preview.yml: -------------------------------------------------------------------------------- 1 | name: 📱 EAS Preview 2 | 3 | env: 4 | EXPO_PUBLIC_API_URL: ${{ vars.API_URL }} 5 | EXPO_PUBLIC_AUTH_URL: ${{ vars.AUTH_URL }} 6 | EXPO_PUBLIC_OPENAPI_URL: ${{ vars.OPENAPI_URL }} 7 | 8 | on: [pull_request] 9 | jobs: 10 | preview: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: 🏗 Setup repo 14 | uses: actions/checkout@v4 15 | 16 | - name: 🏗 Setup pnpm 17 | uses: pnpm/action-setup@v4 18 | with: 19 | version: 10 20 | 21 | - name: 🏗 Setup Node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 'lts/*' 25 | 26 | - name: 🏗 Setup EAS 27 | uses: expo/expo-github-action@v8 28 | with: 29 | eas-version: latest 30 | token: ${{ secrets.EXPO_TOKEN }} 31 | 32 | - name: 📦 Install dependencies 33 | run: pnpm install 34 | 35 | - name: 🚀 Create preview 36 | uses: expo/expo-github-action/preview@v8 37 | with: 38 | # `github.event.pull_request.head.ref` is only available on `pull_request` triggers. 39 | # Use your own, or keep the automatically inferred branch name from `--auto`, when using different triggers. 40 | command: eas update --auto --branch ${{ github.event.pull_request.head.ref }} 41 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconChevronsUpDown.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconChevronsUpDown = ficus(_Svg); 21 | export default IconChevronsUpDown; 22 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-languages.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-power.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconHouseFill.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconHouseFill = ficus(_Svg); 21 | export default IconHouseFill; 22 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-edit-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/svgr.config.cjs: -------------------------------------------------------------------------------- 1 | const comment = 2 | '// Autogenerated file, DO NOT MODIFY or it will be overwritten'; 3 | 4 | const template = (variables, { tpl }) => { 5 | const componentName = variables.componentName.replace('Svg', ''); 6 | return tpl` 7 | ${comment} 8 | ${variables.imports}; 9 | 10 | ${variables.interfaces}; 11 | import { ficus } from 'react-native-ficus-ui'; 12 | 13 | const _Svg = (${variables.props}) => ( 14 | ${variables.jsx} 15 | ); 16 | 17 | const ${componentName} = ficus(_Svg) 18 | 19 | export default ${componentName}; 20 | `; 21 | }; 22 | 23 | // eslint-disable-next-line no-undef 24 | module.exports = { 25 | outDir: './src/components/icons/generated', 26 | typescript: true, 27 | replaceAttrValues: { 28 | '#000': 'currentColor', 29 | none: 'currentColor', 30 | black: 'currentColor', 31 | }, 32 | expandProps: true, 33 | template, 34 | native: true, 35 | svgo: true, 36 | svgoConfig: { 37 | plugins: [ 38 | { 39 | name: 'removeAttrs', 40 | params: { 41 | attrs: '(xmlns)', 42 | }, 43 | }, 44 | ], 45 | }, 46 | 47 | // Prevent svgr + prettier 3 compatibility issue 48 | // It is ok to disable it here, generation script will run prettier 49 | // See https://github.com/gregberge/svgr/issues/893 for details on the underlying issue 50 | prettier: false, 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconFolder.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconFolder = ficus(_Svg); 23 | export default IconFolder; 24 | -------------------------------------------------------------------------------- /src/hooks/use-browser/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MutateOptions, 3 | useMutation, 4 | UseMutationOptions, 5 | } from '@tanstack/react-query'; 6 | import * as WebBrowser from 'expo-web-browser'; 7 | import { toast } from 'sonner-native'; 8 | 9 | type UseBrowserMutationParameters = { 10 | url: string; 11 | options?: WebBrowser.WebBrowserOpenOptions; 12 | }; 13 | 14 | export const useBrowser = ( 15 | mutationOptions?: UseMutationOptions< 16 | WebBrowser.WebBrowserResult, 17 | Error, 18 | UseBrowserMutationParameters 19 | > 20 | ) => { 21 | const mutation = useMutation({ 22 | ...mutationOptions, 23 | mutationFn: async ({ options, url }) => { 24 | return WebBrowser.openBrowserAsync(url, { 25 | ...options, 26 | dismissButtonStyle: 'close', 27 | presentationStyle: WebBrowser.WebBrowserPresentationStyle.FORM_SHEET, 28 | }); 29 | }, 30 | onError: (error, ...params) => { 31 | toast.error(error.name); 32 | mutationOptions?.onError?.(error, ...params); 33 | }, 34 | }); 35 | 36 | return { 37 | ...mutation, 38 | open: ( 39 | url: string, 40 | options?: WebBrowser.WebBrowserOpenOptions, 41 | mutationOptions?: MutateOptions< 42 | WebBrowser.WebBrowserResult, 43 | Error, 44 | UseBrowserMutationParameters 45 | > 46 | ) => mutation.mutate({ url, options }, mutationOptions), 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-book-open-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconAlertCircle.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 33 | ); 34 | const IconAlertCircle = ficus(_Svg); 35 | export default IconAlertCircle; 36 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/components/badge.ts: -------------------------------------------------------------------------------- 1 | import { BadgeProps, defineStyleConfig, Dict } from 'react-native-ficus-ui'; 2 | 3 | export default defineStyleConfig< 4 | BadgeProps, 5 | Dict, 6 | Dict 7 | >({ 8 | defaultProps: { variant: '@primary', size: 'md' }, 9 | sizes: { 10 | xs: { h: 16, fontSize: '2xs', fontWeight: 'medium' }, 11 | sm: { h: 20, fontSize: 'xs', fontWeight: 'medium' }, 12 | md: { h: 24, fontSize: 'sm', fontWeight: 'medium' }, 13 | lg: { h: 32, fontSize: 'md', fontWeight: 'medium' }, 14 | }, 15 | baseStyle: { 16 | px: 8, 17 | textTransform: 'none', 18 | borderRadius: 'md', 19 | }, 20 | variants: { 21 | '@primary': { 22 | backgroundColor: 'brand.900', 23 | color: 'white', 24 | _dark: { 25 | backgroundColor: 'brand.50', 26 | color: 'brand.900', 27 | }, 28 | }, 29 | '@secondary': { 30 | backgroundColor: 'white', 31 | color: 'brand.950', 32 | _dark: { 33 | backgroundColor: 'brand.800', 34 | color: 'brand.50', 35 | }, 36 | }, 37 | '@error': { 38 | backgroundColor: 'negative.100', 39 | color: 'negative.800', 40 | }, 41 | '@warning': { 42 | backgroundColor: 'warning.100', 43 | color: 'warning.800', 44 | }, 45 | '@success': { 46 | backgroundColor: 'positive.100', 47 | color: 'positive.600', 48 | }, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconUser.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconUser = ficus(_Svg); 29 | export default IconUser; 30 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconLock.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconLock = ficus(_Svg); 29 | export default IconLock; 30 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-shopping-cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconRepository.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 21 | ); 22 | const IconRepository = ficus(_Svg); 23 | export default IconRepository; 24 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconHome.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconHome = ficus(_Svg); 29 | export default IconHome; 30 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconMail.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconMail = ficus(_Svg); 29 | export default IconMail; 30 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconHouse.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconHouse = ficus(_Svg); 21 | export default IconHouse; 22 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconFile.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconFile = ficus(_Svg); 29 | export default IconFile; 30 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-eye-closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconCircleUserRound.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconCircleUserRound = ficus(_Svg); 21 | export default IconCircleUserRound; 22 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-house-duotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: 🔎 Code Quality 2 | 3 | env: 4 | EXPO_PUBLIC_API_URL: ${{ vars.API_URL }} 5 | EXPO_PUBLIC_AUTH_URL: ${{ vars.AUTH_URL }} 6 | EXPO_PUBLIC_OPENAPI_URL: ${{ vars.OPENAPI_URL }} 7 | 8 | on: 9 | push: 10 | branches: [main] 11 | pull_request: 12 | 13 | jobs: 14 | linter: 15 | name: 🧹 Linter 16 | timeout-minutes: 10 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: 🏗 Setup repo 20 | uses: actions/checkout@v4 21 | 22 | - name: 🏗 Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 'lts/*' 26 | 27 | - name: 📦 Install pnpm 28 | uses: pnpm/action-setup@v4 29 | with: 30 | version: 10 31 | 32 | - name: 📦 Install dependencies 33 | run: pnpm install 34 | 35 | - name: Run eslint 36 | run: pnpm run lint:eslint 37 | 38 | typescriptChecker: 39 | name: 🟦 TypeScript Checker 40 | timeout-minutes: 10 41 | runs-on: ubuntu-latest 42 | strategy: 43 | matrix: 44 | node: [22, 'lts/*'] 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Install pnpm 49 | uses: pnpm/action-setup@v4 50 | with: 51 | version: 10 52 | 53 | - name: Setup Node.js 54 | uses: actions/setup-node@v4 55 | with: 56 | node-version: ${{ matrix.node }} 57 | cache: 'pnpm' 58 | 59 | - name: Install deps 60 | run: pnpm install 61 | 62 | - name: Run Typescript checker 63 | run: pnpm run lint:ts 64 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconTrash2.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconTrash2 = ficus(_Svg); 21 | export default IconTrash2; 22 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconDevices.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconDevices = ficus(_Svg); 21 | export default IconDevices; 22 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-share-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconCopy.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconCopy = ficus(_Svg); 29 | export default IconCopy; 30 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconBookOpen.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconBookOpen = ficus(_Svg); 29 | export default IconBookOpen; 30 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconXCircle.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 33 | ); 34 | const IconXCircle = ficus(_Svg); 35 | export default IconXCircle; 36 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconCalendar.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 38 | 39 | ); 40 | const IconCalendar = ficus(_Svg); 41 | export default IconCalendar; 42 | -------------------------------------------------------------------------------- /src/lib/ficus-ui/components/button.ts: -------------------------------------------------------------------------------- 1 | import { ButtonProps, defineStyleConfig, Dict } from 'react-native-ficus-ui'; 2 | 3 | export default defineStyleConfig< 4 | ButtonProps, 5 | Dict, 6 | Dict 7 | >({ 8 | defaultProps: { variant: '@primary', size: 'lg' }, 9 | sizes: { 10 | xs: { h: 28, fontSize: 'xs', fontWeight: 'medium' }, 11 | sm: { h: 32, fontSize: 'sm', fontWeight: 'medium' }, 12 | md: { h: 36, fontSize: 'sm', fontWeight: 'medium' }, 13 | lg: { h: 40, fontSize: 'sm', fontWeight: 'medium' }, 14 | }, 15 | baseStyle: { 16 | gap: 4, 17 | borderRadius: 'md', 18 | borderColor: 'brand.200', 19 | _disabled: { opacity: 0.7 }, 20 | _dark: { 21 | borderColor: 'brand.600', 22 | }, 23 | }, 24 | variants: { 25 | '@primary': { 26 | backgroundColor: 'brand.900', 27 | color: 'white', 28 | _dark: { 29 | backgroundColor: 'brand.50', 30 | color: 'brand.900', 31 | }, 32 | }, 33 | '@secondary': { 34 | backgroundColor: 'white', 35 | color: 'brand.950', 36 | borderWidth: 1, 37 | _dark: { 38 | backgroundColor: 'brand.800', 39 | color: 'brand.50', 40 | }, 41 | }, 42 | '@destructive': { 43 | backgroundColor: 'negative.600', 44 | color: 'white', 45 | }, 46 | '@ghost': { 47 | backgroundColor: 'transparent', 48 | color: 'neutral.950', 49 | _dark: { color: 'neutral.100' }, 50 | }, 51 | '@link': { 52 | px: 0, 53 | backgroundColor: 'transparent', 54 | color: 'neutral.950', 55 | _dark: { color: 'neutral.100' }, 56 | }, 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-book-open-duotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconUnlock.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconUnlock = ficus(_Svg); 29 | export default IconUnlock; 30 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconTag.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconTag = ficus(_Svg); 29 | export default IconTag; 30 | -------------------------------------------------------------------------------- /src/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import { Slot } from 'expo-router'; 4 | import * as SplashScreen from 'expo-splash-screen'; 5 | import { FicusProvider } from 'react-native-ficus-ui'; 6 | import { GestureHandlerRootView } from 'react-native-gesture-handler'; 7 | import { 8 | initialWindowMetrics, 9 | SafeAreaProvider, 10 | } from 'react-native-safe-area-context'; 11 | import 'react-native-reanimated'; 12 | import '@/lib/i18n'; 13 | 14 | import theme from '@/lib/ficus-ui/theme'; 15 | 16 | import { ThemeManager } from '@/components/theme-manager'; 17 | import { Sonner } from '@/components/ui/sonner'; 18 | 19 | import { DevTools } from '@/features/devtools/devtools'; 20 | import { SplashScreenManager } from '@/layout/splash-screen-manager'; 21 | 22 | export const queryClient = new QueryClient(); 23 | 24 | // SplashScreen hide management in on src/app/index.tsx 25 | SplashScreen.preventAutoHideAsync(); 26 | 27 | export default function RootLayout() { 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {process.env.NODE_ENV === 'development' && } 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconHash.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 38 | 39 | ); 40 | const IconHash = ficus(_Svg); 41 | export default IconHash; 42 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconLogOut.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 33 | ); 34 | const IconLogOut = ficus(_Svg); 35 | export default IconLogOut; 36 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconLanguages.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 18 | 19 | ); 20 | const IconLanguages = ficus(_Svg); 21 | export default IconLanguages; 22 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconLogIn.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 33 | ); 34 | const IconLogIn = ficus(_Svg); 35 | export default IconLogIn; 36 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconShare.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 33 | ); 34 | const IconShare = ficus(_Svg); 35 | export default IconShare; 36 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconUpload.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 33 | ); 34 | const IconUpload = ficus(_Svg); 35 | export default IconUpload; 36 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-user-circle-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.rnstorybook/storybook.requires.ts: -------------------------------------------------------------------------------- 1 | /* do not change this file, it is auto generated by storybook. */ 2 | import { start, updateView, View } from '@storybook/react-native'; 3 | 4 | import '@storybook/addon-ondevice-controls/register'; 5 | import '@storybook/addon-ondevice-actions/register'; 6 | 7 | const normalizedStories = [ 8 | { 9 | titlePrefix: '', 10 | directory: './.rnstorybook/stories', 11 | files: '**/*.stories.?(ts|tsx|js|jsx)', 12 | importPathMatcher: 13 | /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/, 14 | // @ts-ignore 15 | req: require.context( 16 | './stories', 17 | true, 18 | /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/ 19 | ), 20 | }, 21 | { 22 | titlePrefix: '', 23 | directory: './src', 24 | files: '**/*.stories.@(js|jsx|mjs|ts|tsx)', 25 | importPathMatcher: 26 | /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(js|jsx|mjs|ts|tsx))$/, 27 | // @ts-ignore 28 | req: require.context( 29 | '../src', 30 | true, 31 | /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(js|jsx|mjs|ts|tsx))$/ 32 | ), 33 | }, 34 | ]; 35 | 36 | declare global { 37 | var view: View; 38 | var STORIES: typeof normalizedStories; 39 | } 40 | 41 | const annotations = [require('@storybook/react-native/preview')]; 42 | 43 | global.STORIES = normalizedStories; 44 | 45 | // @ts-ignore 46 | module?.hot?.accept?.(); 47 | 48 | if (!global.view) { 49 | global.view = start({ 50 | annotations, 51 | storyEntries: normalizedStories, 52 | }); 53 | } else { 54 | updateView(global.view, annotations, normalizedStories); 55 | } 56 | 57 | export const view: View = global.view; 58 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconDownload.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 32 | 33 | ); 34 | const IconDownload = ficus(_Svg); 35 | export default IconDownload; 36 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-sliders.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/(logged)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, useRouter } from 'expo-router'; 2 | import { useEffect } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { useThemedStyle } from '@/hooks/use-themed-style'; 6 | 7 | import { authClient } from '@/features/auth/client'; 8 | import { ViewAuthOnboarding } from '@/features/auth/view-auth-onboarding'; 9 | 10 | export default function LoggedLayout() { 11 | const router = useRouter(); 12 | const session = authClient.useSession(); 13 | const { t } = useTranslation(['layout']); 14 | 15 | useEffect(() => { 16 | if (!session.isPending && !session.data?.user) { 17 | router.replace('/(public)/sign-in'); 18 | } 19 | }, [router, session.data?.user, session.isPending]); 20 | 21 | const themedStyle = useThemedStyle(); 22 | 23 | if (!session.data?.user?.name) { 24 | return ; 25 | } 26 | 27 | return ( 28 | 37 | 38 | 42 | {/* Add new logged-in View that's not included in tabs here */} 43 | ({ 46 | headerShown: true, 47 | title: (props.route.params as { title?: string })?.title, 48 | })} 49 | /> 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/icons/svg-sources/icon-key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/generated/IconPower.tsx: -------------------------------------------------------------------------------- 1 | // Autogenerated file, DO NOT MODIFY or it will be overwritten; 2 | import * as React from 'react'; 3 | import { ficus } from 'react-native-ficus-ui'; 4 | import type { SvgProps } from 'react-native-svg'; 5 | import Svg, { Path } from 'react-native-svg'; 6 | const _Svg = (props: SvgProps) => ( 7 | 14 | 20 | 26 | 27 | ); 28 | const IconPower = ficus(_Svg); 29 | export default IconPower; 30 | --------------------------------------------------------------------------------