├── .babelrc ├── .github └── workflows │ ├── checks.yaml │ ├── eas-build.yaml │ └── eas-update.yaml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── .zed └── settings.json ├── AppleWWDRCAG3.cer ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── apps ├── api │ ├── .docker │ │ └── docker-compose.yml │ ├── .env.development │ ├── .gitignore │ ├── README.md │ ├── biome.json │ ├── index.ts │ ├── lib │ │ ├── log.ts │ │ ├── prisma.ts │ │ ├── revenuecat-v1.ts │ │ └── revenuecat-v1.types.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── api │ │ │ ├── [...route].ts │ │ │ └── cron │ │ │ │ └── users │ │ │ │ └── sync-subscriptions.ts │ │ └── index.tsx │ ├── prisma │ │ ├── migrations │ │ │ ├── 20240605095317_init_model │ │ │ │ └── migration.sql │ │ │ ├── 20240606080412_add_full_schemas │ │ │ │ └── migration.sql │ │ │ ├── 20240607025026_remove_currency_from_budget_period_config │ │ │ │ └── migration.sql │ │ │ ├── 20240608073549_budget_invitation_email_unique │ │ │ │ └── migration.sql │ │ │ ├── 20240608175448_add_category_user_id │ │ │ │ └── migration.sql │ │ │ ├── 20240609110108_add_category_type │ │ │ │ └── migration.sql │ │ │ ├── 20240620165734_cascading_transaction_on_wallet_or_user_deleted │ │ │ │ └── migration.sql │ │ │ ├── 20240716180407_add_cached_gpt_response │ │ │ │ └── migration.sql │ │ │ ├── 20240718080526_add_currency_exchange_rate │ │ │ │ └── migration.sql │ │ │ ├── 20240718080935_set_exchange_rate_date_to_string │ │ │ │ └── migration.sql │ │ │ ├── 20240718173810_add_transaction_amount_in_vnd │ │ │ │ └── migration.sql │ │ │ ├── 20240719123618_add_budget_period_weekly_type │ │ │ │ └── migration.sql │ │ │ ├── 20240820081223_cascading_budget_users_on_budget_deleted │ │ │ │ └── migration.sql │ │ │ ├── 20240822091654_cascade_user_relations │ │ │ │ └── migration.sql │ │ │ ├── 20240901110039_remove_budget_period_uniqe_constraint │ │ │ │ └── migration.sql │ │ │ ├── 20240912043336_add_blob_object │ │ │ │ └── migration.sql │ │ │ ├── 20240912044639_update_blob_fields │ │ │ │ └── migration.sql │ │ │ ├── 20240921234642_add_user_entitlement │ │ │ │ └── migration.sql │ │ │ ├── 20240922083143_test │ │ │ │ └── migration.sql │ │ │ ├── 20240922083225_revert_test │ │ │ │ └── migration.sql │ │ │ ├── 20240923035535_add_usermetadata │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── prompts │ │ └── image2transaction.txt │ ├── tsconfig.json │ ├── types │ │ ├── common.ts │ │ └── got-fix.d.ts │ ├── v1 │ │ ├── constants │ │ │ ├── category.const.ts │ │ │ └── wallet.const.ts │ │ ├── index.ts │ │ ├── middlewares │ │ │ ├── auth.ts │ │ │ └── webhook-auth.ts │ │ ├── routes │ │ │ ├── auth.ts │ │ │ ├── budgets.ts │ │ │ ├── categories.ts │ │ │ ├── exchange-rates.ts │ │ │ ├── transactions.ts │ │ │ ├── users.ts │ │ │ ├── utils.ts │ │ │ ├── wallets.ts │ │ │ └── webhooks │ │ │ │ ├── clerk.ts │ │ │ │ └── revenuecat.ts │ │ └── services │ │ │ ├── ai-cache.service.ts │ │ │ ├── ai.service.ts │ │ │ ├── blob.service.ts │ │ │ ├── budget-invitation.service.ts │ │ │ ├── budget.service.ts │ │ │ ├── category.service.ts │ │ │ ├── clerk.service.ts │ │ │ ├── exchange-rates.service.ts │ │ │ ├── file.service.ts │ │ │ ├── revenue-cat.service.ts │ │ │ ├── transaction.service.ts │ │ │ ├── user-metadata.service.ts │ │ │ ├── user.service.ts │ │ │ └── wallet.service.ts │ └── vercel.json └── mobile │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── ai │ │ │ │ └── sixpm │ │ │ │ └── app │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainApplication.kt │ │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── splashscreen_image.png │ │ │ ├── drawable-mdpi │ │ │ └── splashscreen_image.png │ │ │ ├── drawable-xhdpi │ │ │ └── splashscreen_image.png │ │ │ ├── drawable-xxhdpi │ │ │ └── splashscreen_image.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── splashscreen_image.png │ │ │ ├── drawable │ │ │ ├── rn_edit_text_material.xml │ │ │ └── splashscreen.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── dark.png │ │ │ ├── digital.png │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_round.png │ │ │ ├── light.png │ │ │ └── original.png │ │ │ ├── mipmap-mdpi │ │ │ ├── dark.png │ │ │ ├── digital.png │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_round.png │ │ │ ├── light.png │ │ │ └── original.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── dark.png │ │ │ ├── digital.png │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_round.png │ │ │ ├── light.png │ │ │ └── original.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── dark.png │ │ │ ├── digital.png │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_round.png │ │ │ ├── light.png │ │ │ └── original.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── dark.png │ │ │ ├── digital.png │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_round.png │ │ │ ├── light.png │ │ │ └── original.png │ │ │ ├── values-night │ │ │ └── colors.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── react-settings-plugin │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── kotlin │ │ │ └── expo │ │ │ └── plugins │ │ │ └── ReactSettingsPlugin.kt │ ├── sentry.properties │ └── settings.gradle │ ├── app.json │ ├── app │ ├── (app) │ │ ├── (tabs) │ │ │ ├── _layout.tsx │ │ │ ├── budgets.tsx │ │ │ ├── index.tsx │ │ │ └── settings.tsx │ │ ├── _layout.tsx │ │ ├── app-icon.tsx │ │ ├── appearance.tsx │ │ ├── blob-viewer.tsx │ │ ├── budget │ │ │ ├── [budgetId] │ │ │ │ ├── edit.tsx │ │ │ │ └── index.tsx │ │ │ └── new-budget.tsx │ │ ├── category │ │ │ ├── [categoryId].tsx │ │ │ ├── index.tsx │ │ │ └── new-category.tsx │ │ ├── feedback.tsx │ │ ├── language.tsx │ │ ├── paywall.tsx │ │ ├── profile.tsx │ │ ├── review-transactions.tsx │ │ ├── transaction │ │ │ ├── [transactionId].tsx │ │ │ └── new-record.tsx │ │ └── wallet │ │ │ ├── [walletId].tsx │ │ │ ├── accounts.tsx │ │ │ └── new-account.tsx │ ├── (auth) │ │ ├── _layout.tsx │ │ └── login.tsx │ ├── (aux) │ │ ├── _layout.tsx │ │ ├── privacy-policy.tsx │ │ └── terms-of-service.tsx │ ├── +html.tsx │ ├── +not-found.tsx │ ├── _layout.tsx │ └── onboarding │ │ ├── _layout.tsx │ │ ├── step-one.tsx │ │ ├── step-three.tsx │ │ └── step-two.tsx │ ├── assets │ ├── fonts │ │ ├── Haskoy-Bold.ttf │ │ ├── Haskoy-Medium.ttf │ │ ├── Haskoy-Regular.ttf │ │ └── Haskoy-SemiBold.ttf │ └── images │ │ ├── adaptive-icon.png │ │ ├── app-icons │ │ ├── dark.png │ │ ├── digital.png │ │ ├── light.png │ │ └── original.png │ │ ├── favicon.png │ │ ├── icon.png │ │ ├── paywall-images │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ │ └── splash.png │ ├── babel.config.js │ ├── biome.json │ ├── components.json │ ├── components │ ├── __tests__ │ │ ├── ThemedText-test.tsx │ │ └── __snapshots__ │ │ │ └── ThemedText-test.tsx.snap │ ├── auth │ │ ├── auth-email.tsx │ │ ├── auth-local.tsx │ │ ├── auth-social.tsx │ │ └── emailSchema.tsx │ ├── budget │ │ ├── budget-form.tsx │ │ ├── budget-item.tsx │ │ ├── budget-statistic.tsx │ │ ├── period-control.tsx │ │ ├── period-range-field.tsx │ │ ├── select-budget-type-field.tsx │ │ └── select-period-type-field.tsx │ ├── category │ │ ├── category-form.tsx │ │ ├── category-item.tsx │ │ └── select-category-icon-field.tsx │ ├── common │ │ ├── add-new-button.tsx │ │ ├── amount-format.tsx │ │ ├── back-button.tsx │ │ ├── bottom-sheet.tsx │ │ ├── burndown-chart │ │ │ ├── active-value-indicator.tsx │ │ │ ├── burndown-chart.tsx │ │ │ ├── diff-badge.tsx │ │ │ └── index.ts │ │ ├── circular-progress.tsx │ │ ├── currency-sheet.tsx │ │ ├── custom-palette-wrapper.tsx │ │ ├── date-picker.tsx │ │ ├── date-range-picker.tsx │ │ ├── footer-gradient.tsx │ │ ├── generic-icon.tsx │ │ ├── icon-grid-sheet.tsx │ │ ├── list-skeleton.tsx │ │ ├── logo.tsx │ │ ├── marquee.tsx │ │ ├── menu-item.tsx │ │ ├── tab-bar.tsx │ │ ├── toast.tsx │ │ ├── toolbar.tsx │ │ ├── update-loader.tsx │ │ └── user-avatar.tsx │ ├── form-fields │ │ ├── boolean-field.tsx │ │ ├── currency-field.tsx │ │ ├── input-field.tsx │ │ └── submit-button.tsx │ ├── home │ │ ├── category-chart.tsx │ │ ├── header.tsx │ │ ├── select-filter.tsx │ │ ├── select-wallet-account.tsx │ │ ├── time-range-control.tsx │ │ └── wallet-statistics.tsx │ ├── numeric-pad │ │ ├── index.ts │ │ └── numeric-pad.tsx │ ├── primitives │ │ ├── avatar │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── label │ │ │ ├── index.ts │ │ │ ├── label.tsx │ │ │ ├── label.web.tsx │ │ │ └── types.ts │ │ ├── separator │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── slot.tsx │ │ ├── switch │ │ │ ├── index.ts │ │ │ ├── switch.tsx │ │ │ ├── switch.web.tsx │ │ │ └── types.ts │ │ ├── tabs │ │ │ ├── index.ts │ │ │ ├── tabs.tsx │ │ │ ├── tabs.web.tsx │ │ │ └── types.ts │ │ └── types.ts │ ├── scanner │ │ ├── animated-ring.tsx │ │ ├── scanning-indicator.tsx │ │ └── scanning-overlay.tsx │ ├── setting │ │ ├── profile-card.tsx │ │ ├── select-default-currency.tsx │ │ └── set-local-auth.tsx │ ├── svg-assets │ │ ├── apple-logo.tsx │ │ ├── auth-illustration.tsx │ │ ├── google-logo.tsx │ │ ├── notification-illustration.tsx │ │ ├── onboard-illustration.tsx │ │ ├── paywall-illustration.tsx │ │ └── update-illustration.tsx │ ├── text-ticker │ │ ├── index.ts │ │ └── text-ticker.tsx │ ├── transaction │ │ ├── draft-transaction-item.tsx │ │ ├── draft-transaction-list.tsx │ │ ├── handy-arrow.tsx │ │ ├── scanner.tsx │ │ ├── select-account-field.tsx │ │ ├── select-budget-field.tsx │ │ ├── select-category-field.tsx │ │ ├── transaction-form.tsx │ │ └── transaction-item.tsx │ ├── ui │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── collapsible.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── progress.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── skeleton.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ └── text.tsx │ └── wallet │ │ ├── account-form.tsx │ │ ├── select-account-icon-field.tsx │ │ ├── select-balance-state-field.tsx │ │ └── wallet-account-item.tsx │ ├── eas.json │ ├── global.css │ ├── hooks │ ├── use-color-palette.tsx │ ├── use-feature-flag.tsx │ ├── use-local-auth.tsx │ ├── use-ota-updates.tsx │ ├── use-purchases.ts │ ├── use-schedule-notification.tsx │ ├── use-seed.tsx │ ├── use-user-metadata.tsx │ ├── use-warm-up-browser.tsx │ └── useColorScheme.ts │ ├── ios │ ├── .gitignore │ ├── .xcode.env │ ├── 6pm.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── 6pm.xcscheme │ ├── 6pm.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── 6pm │ │ ├── 6pm-Bridging-Header.h │ │ ├── 6pm.entitlements │ │ ├── AppDelegate.h │ │ ├── AppDelegate.mm │ │ ├── DynamicAppIcons │ │ │ ├── dark-Icon-$60x60@2x.png │ │ │ ├── dark-Icon-$60x60@2x~ipad.png │ │ │ ├── dark-Icon-$60x60@3x.png │ │ │ ├── dark-Icon-$60x60@3x~ipad.png │ │ │ ├── digital-Icon-$60x60@2x.png │ │ │ ├── digital-Icon-$60x60@2x~ipad.png │ │ │ ├── digital-Icon-$60x60@3x.png │ │ │ ├── digital-Icon-$60x60@3x~ipad.png │ │ │ ├── light-Icon-$60x60@2x.png │ │ │ ├── light-Icon-$60x60@2x~ipad.png │ │ │ ├── light-Icon-$60x60@3x.png │ │ │ ├── light-Icon-$60x60@3x~ipad.png │ │ │ ├── original-Icon-$60x60@2x.png │ │ │ ├── original-Icon-$60x60@2x~ipad.png │ │ │ ├── original-Icon-$60x60@3x.png │ │ │ └── original-Icon-$60x60@3x~ipad.png │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── App-Icon-1024x1024@1x.png │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── SplashScreen.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── image.png │ │ │ └── SplashScreenBackground.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── image.png │ │ ├── Info.plist │ │ ├── PrivacyInfo.xcprivacy │ │ ├── SplashScreen.storyboard │ │ ├── Supporting │ │ │ └── Expo.plist │ │ ├── main.m │ │ └── noop-file.swift │ ├── Podfile │ ├── Podfile.lock │ ├── Podfile.properties.json │ └── sentry.properties │ ├── lib │ ├── cache.ts │ ├── client.ts │ ├── constaints.ts │ ├── date.tsx │ ├── icons │ │ ├── category-icons.ts │ │ ├── iconWithClassName.ts │ │ └── wallet-icons.ts │ ├── theme.ts │ └── utils.ts │ ├── locales │ ├── en │ │ ├── messages.po │ │ └── messages.ts │ ├── provider.tsx │ └── vi │ │ ├── messages.po │ │ └── messages.ts │ ├── metro.config.js │ ├── mutations │ ├── transaction.ts │ └── user.ts │ ├── nativewind-env.d.ts │ ├── package.json │ ├── queries │ └── auth.ts │ ├── scripts │ └── reset-project.js │ ├── stores │ ├── budget │ │ ├── hooks.tsx │ │ ├── queries.ts │ │ └── store.ts │ ├── category │ │ ├── hooks.tsx │ │ ├── queries.ts │ │ └── store.ts │ ├── core │ │ ├── store-interval-update.tsx │ │ ├── store-provider.tsx │ │ ├── stores.const.ts │ │ ├── stores.d.ts │ │ └── use-reset-all-stores.tsx │ ├── exchange-rates │ │ ├── hooks.tsx │ │ ├── queries.ts │ │ └── store.ts │ ├── transaction │ │ ├── hooks.tsx │ │ ├── queries.ts │ │ └── store.ts │ ├── user-settings │ │ ├── hooks.ts │ │ └── store.ts │ └── wallet │ │ ├── hooks.tsx │ │ ├── queries.ts │ │ └── store.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── biome.json ├── github.png ├── lefthook.yml ├── lingui.config.js ├── package.json ├── packages ├── currency │ ├── README.md │ ├── package.json │ └── src │ │ ├── currencies.json │ │ ├── formatter.ts │ │ └── index.ts ├── utilities │ ├── README.md │ ├── biome.json │ ├── package.json │ └── src │ │ ├── config.ts │ │ ├── date │ │ ├── dayjs.ts │ │ └── period.ts │ │ ├── index.ts │ │ └── transactions.ts └── validation │ ├── README.md │ ├── biome.json │ ├── package.json │ └── src │ ├── auth.zod.ts │ ├── budget.zod.ts │ ├── category.zod.ts │ ├── index.ts │ ├── prisma │ └── index.ts │ ├── transaction.zod.ts │ ├── user.zod.ts │ └── wallet.zod.ts ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["macros"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*prisma* 2 | node-linker=hoisted 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "emeraldwalk.runonsave", 4 | "biomejs.biome", 5 | "streetsidesoftware.code-spell-checker", 6 | "prisma.prisma", 7 | "bradlc.vscode-tailwindcss", 8 | "streetsidesoftware.code-spell-checker-vietnamese" 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "biomejs.biome", 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports.biome": "explicit" 5 | }, 6 | "[json]": { 7 | "editor.defaultFormatter": "biomejs.biome" 8 | }, 9 | "[typescript]": { 10 | "editor.defaultFormatter": "biomejs.biome" 11 | }, 12 | "[typescriptreact]": { 13 | "editor.defaultFormatter": "biomejs.biome" 14 | }, 15 | "editor.formatOnSave": true, 16 | "cSpell.words": [ 17 | "EXCHANGERATES", 18 | "hono", 19 | "openai", 20 | "posthog", 21 | "revenuecat", 22 | "svix", 23 | "tanstack" 24 | ], 25 | "emeraldwalk.runonsave": { 26 | "commands": [ 27 | { 28 | "match": "\\.(ts|tsx|js|jsx|html)$", 29 | "cmd": "pnpm biome lint ${file} --unsafe" 30 | } 31 | ] 32 | } 33 | } -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | // Folder-specific settings 2 | // 3 | // For a full list of overridable settings, and general information on folder-specific settings, 4 | // see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings 5 | { 6 | "format_on_save": "on", 7 | "code_actions_on_format": { 8 | "source.fixAll": true, 9 | "source.organizeImports.biome": true 10 | }, 11 | "formatter": "language_server" 12 | } 13 | -------------------------------------------------------------------------------- /AppleWWDRCAG3.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/AppleWWDRCAG3.cer -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install-act-brew: 2 | @echo "Installing act..." 3 | brew install act 4 | 5 | install-act: 6 | @echo "Installing act..." 7 | curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash 8 | 9 | eas-build: 10 | @echo "Building EAS..." 11 | act --var ACT=true -W .github/workflows/eas-build.yaml --secret-file apps/mobile/.env.production.local -P macos-latest=-self-hosted --input auto_submit=true 12 | 13 | eas-update: 14 | @echo "Updating EAS..." 15 | act --var ACT=true -W .github/workflows/eas-update.yaml --secret-file apps/mobile/.env.production.local -------------------------------------------------------------------------------- /apps/api/.docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Use postgres/example user/password credentials 2 | version: '3.9' 3 | name: 6pm 4 | services: 5 | db: 6 | image: postgres 7 | restart: always 8 | shm_size: 128mb 9 | environment: 10 | POSTGRES_PASSWORD: postgres 11 | ports: 12 | - "5432:5432" 13 | -------------------------------------------------------------------------------- /apps/api/.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" 2 | LOG_LEVEL="debug" 3 | TZ="UTC" 4 | DATABASE_PRISMA_URL="postgresql://postgres:postgres@localhost:5432/6pm" 5 | DATABASE_URL_NON_POOLING="postgresql://postgres:postgres@localhost:5432/6pm" 6 | SHADOW_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/6pm-shadow" 7 | -------------------------------------------------------------------------------- /apps/api/.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .vercel/ 3 | .yarn/ 4 | !.yarn/releases 5 | .vscode/* 6 | !.vscode/launch.json 7 | !.vscode/*.code-snippets 8 | .idea/workspace.xml 9 | .idea/usage.statistics.xml 10 | .idea/shelf 11 | 12 | # deps 13 | node_modules/ 14 | 15 | # env 16 | .env 17 | .env.production 18 | 19 | # logs 20 | logs/ 21 | *.log 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | pnpm-debug.log* 26 | lerna-debug.log* 27 | 28 | # misc 29 | .DS_Store 30 | .vercel 31 | -------------------------------------------------------------------------------- /apps/api/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm run start 4 | ``` 5 | -------------------------------------------------------------------------------- /apps/api/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.0/schema.json", 3 | "extends": ["../../biome.json"], 4 | "files": { 5 | "include": ["**/*.ts"], 6 | "ignore": ["**/*.d.ts"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { except } from 'hono/combine' 3 | import { compress } from 'hono/compress' 4 | import { logger } from 'hono/logger' 5 | import { prettyJSON } from 'hono/pretty-json' 6 | import { requestId } from 'hono/request-id' 7 | import { trimTrailingSlash } from 'hono/trailing-slash' 8 | 9 | import { log } from './lib/log' 10 | import { hono as appV1 } from './v1' 11 | 12 | const IS_VERCEL = process.env.VERCEL === '1' 13 | 14 | const app = new Hono({ strict: true }) 15 | 16 | // * Global middlewares 17 | .use(compress()) 18 | .use(requestId()) 19 | .use(trimTrailingSlash()) 20 | .use(prettyJSON({ space: 2 })) 21 | .use(except(() => IS_VERCEL, logger(log.info.bind(log)))) 22 | 23 | // * Mounting versioned APIs 24 | .route('/v1', appV1) 25 | 26 | export { app } 27 | export type AppType = typeof app 28 | -------------------------------------------------------------------------------- /apps/api/lib/log.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino' 2 | 3 | const IS_DEV = process.env.NODE_ENV === 'development' 4 | const IS_PROD = process.env.NODE_ENV === 'production' 5 | 6 | export const log = pino({ 7 | level: process.env.LOG_LEVEL || 'debug', 8 | transport: IS_DEV 9 | ? { 10 | target: 'pino-pretty', 11 | } 12 | : undefined, 13 | base: IS_PROD 14 | ? { 15 | env: process.env.NODE_ENV, 16 | revision: process.env.VERCEL_GIT_COMMIT_SHA, 17 | } 18 | : undefined, 19 | }) 20 | 21 | export const getLogger = (name: string) => log.child({ name }) 22 | -------------------------------------------------------------------------------- /apps/api/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | // import { Pool } from '@neondatabase/serverless' 2 | // import { PrismaNeon } from '@prisma/adapter-neon' 3 | import { PrismaClient } from '@prisma/client' 4 | 5 | // const neon = new Pool({ connectionString: process.env.POSTGRES_PRISMA_URL }) 6 | // const adapter = new PrismaNeon(neon) 7 | 8 | const prisma = new PrismaClient() 9 | 10 | // if (process.env.NODE_ENV === 'production') { 11 | // prisma = new PrismaClient() 12 | // } else { 13 | // const globalWithPrisma = global as typeof globalThis & { 14 | // prisma: PrismaClient 15 | // } 16 | // if (!globalWithPrisma.prisma) { 17 | // globalWithPrisma.prisma = new PrismaClient() 18 | // } 19 | // prisma = globalWithPrisma.prisma 20 | // } 21 | 22 | export default prisma 23 | -------------------------------------------------------------------------------- /apps/api/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/api/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | rewrites: () => [{ source: "/v1/:path*", destination: "/api/v1/:path*" }], 5 | transpilePackages: ["@6pm/validation", "@6pm/utilities"], 6 | logging: false, 7 | }; 8 | 9 | module.exports = nextConfig; 10 | -------------------------------------------------------------------------------- /apps/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@6pm/api", 3 | "version": "1.0.2", 4 | "license": "GPL-3.0", 5 | "main": "index.ts", 6 | "private": true, 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "prisma generate && next build", 10 | "start": "next start", 11 | "prisma:migrate": "prisma migrate dev", 12 | "prisma:generate": "prisma generate", 13 | "prisma:reset": "prisma migrate reset --force", 14 | "prisma:studio": "prisma studio", 15 | "postinstall": "prisma generate", 16 | "check": "biome check" 17 | }, 18 | "dependencies": { 19 | "@6pm/utilities": "workspace:^", 20 | "@6pm/validation": "workspace:^", 21 | "@clerk/backend": "^1.2.4", 22 | "@clerk/clerk-sdk-node": "^5.0.42", 23 | "@hono/clerk-auth": "^2.0.0", 24 | "@hono/node-server": "^1.11.4", 25 | "@hono/zod-validator": "^0.2.2", 26 | "@prisma/client": "5.19.1", 27 | "@vercel/blob": "^0.23.4", 28 | "got": "^14.4.1", 29 | "hono": "^4.4.8", 30 | "next": "^14.2.4", 31 | "openai": "^4.52.7", 32 | "pino": "^9.2.0", 33 | "pino-pretty": "^11.2.1", 34 | "prisma": "5.19.1", 35 | "react": "18.3.1", 36 | "react-dom": "18.3.1", 37 | "svix": "^1.28.0", 38 | "zod": "3.23.8", 39 | "zod-prisma-types": "^3.1.8" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "18.11.18", 43 | "@types/react": "18.3.3", 44 | "@types/react-dom": "18.3.0", 45 | "typescript": "^5.5.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/api/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | 3 | export default function App({ Component, pageProps }: AppProps) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /apps/api/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /apps/api/pages/api/[...route].ts: -------------------------------------------------------------------------------- 1 | import { handle } from '@hono/node-server/vercel' 2 | import type { PageConfig } from 'next' 3 | import { app } from '../..' 4 | 5 | export const config: PageConfig = { 6 | api: { 7 | bodyParser: false, 8 | }, 9 | } 10 | 11 | export default handle(app) 12 | -------------------------------------------------------------------------------- /apps/api/pages/api/cron/users/sync-subscriptions.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import { syncAllUsersSubscription } from '../../../../v1/services/user.service' 3 | 4 | export const dynamic = 'force-dynamic' // static by default, unless reading the request 5 | 6 | export default async function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse, 9 | ) { 10 | const authHeader = req.headers.authorization 11 | 12 | if ( 13 | !process.env.CRON_SECRET || 14 | authHeader !== `Bearer ${process.env.CRON_SECRET}` 15 | ) { 16 | return res.status(401).json({ success: false, message: 'unauthorized' }) 17 | } 18 | 19 | res.status(200).json(await syncAllUsersSubscription()) 20 | } 21 | -------------------------------------------------------------------------------- /apps/api/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export default function Home() { 4 | const [message, setMessage] = useState() 5 | 6 | useEffect(() => { 7 | const fetchData = async () => { 8 | const res = await fetch('/api/hello') 9 | const { message } = await res.json() 10 | setMessage(message) 11 | } 12 | fetchData() 13 | }, []) 14 | 15 | if (!message) return

Loading...

16 | 17 | return

{message}

18 | } 19 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240605095317_init_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "email" TEXT NOT NULL, 5 | "name" TEXT, 6 | 7 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 12 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240607025026_remove_currency_from_budget_period_config/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `currency` on the `BudgetPeriodConfig` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "BudgetPeriodConfig" DROP COLUMN "currency"; 9 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240608073549_budget_invitation_email_unique/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[email,budgetId]` on the table `BudgetUserInvitation` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "BudgetUserInvitation_email_budgetId_key" ON "BudgetUserInvitation"("email", "budgetId"); 9 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240608175448_add_category_user_id/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `userId` to the `Category` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Category" ADD COLUMN "userId" TEXT NOT NULL; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "Category" ADD CONSTRAINT "Category_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240609110108_add_category_type/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `type` to the `Category` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- CreateEnum 8 | CREATE TYPE "CategoryType" AS ENUM ('INCOME', 'EXPENSE'); 9 | 10 | -- AlterTable 11 | ALTER TABLE "Category" ADD COLUMN "type" "CategoryType" NOT NULL; 12 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240620165734_cascading_transaction_on_wallet_or_user_deleted/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Transaction" DROP CONSTRAINT "Transaction_createdByUserId_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "Transaction" DROP CONSTRAINT "Transaction_walletAccountId_fkey"; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_walletAccountId_fkey" FOREIGN KEY ("walletAccountId") REFERENCES "UserWalletAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240716180407_add_cached_gpt_response/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "CachedGptResponse" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "query" TEXT NOT NULL, 7 | "response" TEXT NOT NULL, 8 | 9 | CONSTRAINT "CachedGptResponse_pkey" PRIMARY KEY ("id") 10 | ); 11 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240718080526_add_currency_exchange_rate/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "CurrencyExchangeRate" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "fromCurrency" TEXT NOT NULL, 7 | "toCurrency" TEXT NOT NULL, 8 | "rate" DECIMAL(65,30) NOT NULL, 9 | "date" TIMESTAMP(3) NOT NULL, 10 | 11 | CONSTRAINT "CurrencyExchangeRate_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "CurrencyExchangeRate_fromCurrency_toCurrency_date_key" ON "CurrencyExchangeRate"("fromCurrency", "toCurrency", "date"); 16 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240718080935_set_exchange_rate_date_to_string/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "CurrencyExchangeRate" ALTER COLUMN "date" SET DATA TYPE TEXT; 3 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240718173810_add_transaction_amount_in_vnd/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `amountInVnd` to the `Transaction` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Transaction" ADD COLUMN "amountInVnd" DECIMAL(65,30) NOT NULL; 9 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240719123618_add_budget_period_weekly_type/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "BudgetPeriodType" ADD VALUE 'WEEKLY'; 3 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240820081223_cascading_budget_users_on_budget_deleted/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "BudgetUser" DROP CONSTRAINT "BudgetUser_budgetId_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "BudgetUser" DROP CONSTRAINT "BudgetUser_userId_fkey"; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "BudgetUser" ADD CONSTRAINT "BudgetUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "BudgetUser" ADD CONSTRAINT "BudgetUser_budgetId_fkey" FOREIGN KEY ("budgetId") REFERENCES "Budget"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240822091654_cascade_user_relations/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "BudgetUserInvitation" DROP CONSTRAINT "BudgetUserInvitation_createdByUserId_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "UserWalletAccount" DROP CONSTRAINT "UserWalletAccount_userId_fkey"; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "UserWalletAccount" ADD CONSTRAINT "UserWalletAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "BudgetUserInvitation" ADD CONSTRAINT "BudgetUserInvitation_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240901110039_remove_budget_period_uniqe_constraint/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "BudgetPeriodConfig_budgetId_key"; 3 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240912043336_add_blob_object/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "BlobObject" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "uploadedByUserId" TEXT, 7 | "pathname" TEXT NOT NULL, 8 | "contentType" TEXT NOT NULL, 9 | "contentDisposition" TEXT, 10 | "url" TEXT NOT NULL, 11 | "downloadUrl" TEXT NOT NULL, 12 | "transactionId" TEXT, 13 | 14 | CONSTRAINT "BlobObject_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- AddForeignKey 18 | ALTER TABLE "BlobObject" ADD CONSTRAINT "BlobObject_uploadedByUserId_fkey" FOREIGN KEY ("uploadedByUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "BlobObject" ADD CONSTRAINT "BlobObject_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction"("id") ON DELETE SET NULL ON UPDATE CASCADE; 22 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240912044639_update_blob_fields/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `contentDisposition` on table `BlobObject` required. This step will fail if there are existing NULL values in that column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "BlobObject" ALTER COLUMN "contentType" DROP NOT NULL, 9 | ALTER COLUMN "contentDisposition" SET NOT NULL; 10 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240921234642_add_user_entitlement/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "entitlement" TEXT, 3 | ADD COLUMN "entitlementExpiresAt" TIMESTAMP(3), 4 | ADD COLUMN "entitlementProductIdentifier" TEXT; 5 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240922083143_test/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "UserWalletAccount" ADD COLUMN "foo" TEXT; 3 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240922083225_revert_test/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `foo` on the `UserWalletAccount` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "UserWalletAccount" DROP COLUMN "foo"; 9 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20240923035535_add_usermetadata/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "UserMetadata" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "userId" TEXT NOT NULL, 7 | "timezone" TEXT NOT NULL, 8 | 9 | CONSTRAINT "UserMetadata_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateIndex 13 | CREATE UNIQUE INDEX "UserMetadata_userId_key" ON "UserMetadata"("userId"); 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "UserMetadata" ADD CONSTRAINT "UserMetadata_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "composite": true 22 | }, 23 | "include": [ 24 | "next-env.d.ts", 25 | "**/*.ts", 26 | "**/*.tsx" 27 | ], 28 | "exclude": [ 29 | "node_modules" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /apps/api/types/common.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '@prisma/client' 2 | import type { RequestIdVariables } from 'hono/request-id' 3 | 4 | declare module 'hono' { 5 | interface ContextVariableMap { 6 | user: User | null 7 | userId: string | null 8 | requestId: RequestIdVariables['requestId'] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/types/got-fix.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'got' { 2 | import * as got from 'got/dist/source' 3 | export = got 4 | } 5 | -------------------------------------------------------------------------------- /apps/api/v1/constants/category.const.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_INCOME_CATEGORIES = [ 2 | { 3 | en: 'Salary', 4 | vi: 'Tiền lương', 5 | icon: 'BriefcaseBusiness', 6 | }, 7 | { 8 | en: 'Gift', 9 | vi: 'Quà tặng', 10 | icon: 'Clover', 11 | }, 12 | { 13 | en: 'Investment', 14 | vi: 'Đầu tư', 15 | icon: 'ChartLine', 16 | }, 17 | { 18 | en: 'Interest', 19 | vi: 'Lãi suất', 20 | icon: 'BadgePercent', 21 | }, 22 | { 23 | en: 'Other Income', 24 | vi: 'Thu nhập khác', 25 | icon: 'HandCoins', 26 | }, 27 | ] 28 | 29 | export const DEFAULT_EXPENSE_CATEGORIES = [ 30 | { 31 | en: 'Groceries', 32 | vi: 'Nhu yếu phẩm', 33 | icon: 'ShoppingBasket', 34 | }, 35 | { 36 | en: 'Mortgage', 37 | vi: 'Tiền nhà', 38 | icon: 'House', 39 | }, 40 | { 41 | en: 'Bills & Utilities', 42 | vi: 'Hóa đơn & Tiện ích', 43 | icon: 'HousePlug', 44 | }, 45 | { 46 | en: 'Food & Beverage', 47 | vi: 'Thức ăn & Đồ uống', 48 | icon: 'UtensilsCrossed', 49 | }, 50 | { 51 | en: 'Transportation', 52 | vi: 'Di chuyển', 53 | icon: 'CarFront', 54 | }, 55 | { 56 | en: 'Shopping', 57 | vi: 'Mua sắm', 58 | icon: 'ShoppingBag', 59 | }, 60 | { 61 | en: 'Health', 62 | vi: 'Sức khỏe', 63 | icon: 'HeartPulse', 64 | }, 65 | { 66 | en: 'Entertainment', 67 | vi: 'Giải trí', 68 | icon: 'Drama', 69 | }, 70 | { 71 | en: 'Education', 72 | vi: 'Giáo dục', 73 | icon: 'BookHeart', 74 | }, 75 | { 76 | en: 'Investment', 77 | vi: 'Đầu tư', 78 | icon: 'ChartLine', 79 | }, 80 | { 81 | en: 'Other Expense', 82 | vi: 'Chi phí khác', 83 | icon: 'Coins', 84 | }, 85 | ] 86 | -------------------------------------------------------------------------------- /apps/api/v1/constants/wallet.const.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_WALLETS = [ 2 | { 3 | en: 'Cash', 4 | vi: 'Tiền mặt', 5 | icon: 'WalletMinimal', 6 | }, 7 | { 8 | en: 'Bank Account', 9 | vi: 'Tài khoản ngân hàng', 10 | icon: 'Landmark', 11 | }, 12 | { 13 | en: 'Credit Card', 14 | vi: 'Thẻ tín dụng', 15 | icon: 'CreditCard', 16 | }, 17 | // Limit to 3 wallets for free users 18 | // { 19 | // en: 'Investment', 20 | // vi: 'Đầu tư', 21 | // icon: 'ChartLine', 22 | // }, 23 | // { 24 | // en: 'Other Wallet', 25 | // vi: 'Ví khác', 26 | // icon: 'Banknote', 27 | // }, 28 | ] 29 | -------------------------------------------------------------------------------- /apps/api/v1/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { authMiddleware } from './middlewares/auth' 3 | import authApp from './routes/auth' 4 | import budgetsApp from './routes/budgets' 5 | import categoriesApp from './routes/categories' 6 | import exchangeRatesApp from './routes/exchange-rates' 7 | import transactionsApp from './routes/transactions' 8 | import usersApp from './routes/users' 9 | import walletsApp from './routes/wallets' 10 | import clerkWebhooksApp from './routes/webhooks/clerk' 11 | import revenuecatWebhooksApp from './routes/webhooks/revenuecat' 12 | 13 | export const hono = new Hono() 14 | .get('/health', (c) => c.text('ok')) 15 | .route('/webhooks/clerk', clerkWebhooksApp) 16 | .route('/webhooks/revenuecat', revenuecatWebhooksApp) 17 | 18 | .use('*', authMiddleware) 19 | .route('/auth', authApp) 20 | .route('/budgets', budgetsApp) 21 | .route('/categories', categoriesApp) 22 | .route('/users', usersApp) 23 | .route('/transactions', transactionsApp) 24 | .route('/wallets', walletsApp) 25 | .route('/exchange-rates', exchangeRatesApp) 26 | -------------------------------------------------------------------------------- /apps/api/v1/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware, getAuth } from '@hono/clerk-auth' 2 | import type { User } from '@prisma/client' 3 | import type { Context } from 'hono' 4 | import { createMiddleware } from 'hono/factory' 5 | import { HTTPException } from 'hono/http-exception' 6 | import { findUserById } from '../services/user.service' 7 | 8 | export const authMiddleware = createMiddleware(async (c, next) => { 9 | await clerkMiddleware()(c, () => Promise.resolve()) 10 | const auth = getAuth(c) 11 | 12 | if (!auth?.userId) { 13 | c.set('userId', null) 14 | c.set('user', null) 15 | return c.json({ message: 'unauthorized' }, 401) 16 | } 17 | 18 | c.set('userId', auth.userId) 19 | 20 | const user = await findUserById(auth.userId) 21 | 22 | c.set('user', user) 23 | c.header('x-user-id', auth.userId) 24 | 25 | await next() 26 | }) 27 | 28 | export const getAuthUser = (c: Context) => c.get('user') as User | null 29 | 30 | export const getAuthUserStrict = (c: Context) => { 31 | const user = getAuthUser(c) 32 | if (!user) { 33 | throw new HTTPException(401, { message: 'unauthorized' }) 34 | } 35 | return user 36 | } 37 | -------------------------------------------------------------------------------- /apps/api/v1/middlewares/webhook-auth.ts: -------------------------------------------------------------------------------- 1 | import { createMiddleware } from 'hono/factory' 2 | import { Webhook } from 'svix' 3 | import { getLogger } from '../../lib/log' 4 | 5 | export const webhookAuthMiddleware = createMiddleware(async (c, next) => { 6 | const { path } = c.req 7 | 8 | if (path.includes('clerk')) { 9 | const { CLERK_WEBHOOK_SECRET_KEY } = process.env 10 | 11 | if (!CLERK_WEBHOOK_SECRET_KEY) { 12 | return c.json({ message: 'CLERK_WEBHOOK_SECRET_KEY is not set' }, 500) 13 | } 14 | 15 | const bodyText = await c.req.text() 16 | const svix = new Webhook(CLERK_WEBHOOK_SECRET_KEY) 17 | 18 | try { 19 | svix.verify(bodyText, { 20 | 'svix-id': c.req.header('svix-id')!, 21 | 'svix-timestamp': c.req.header('svix-timestamp')!, 22 | 'svix-signature': c.req.header('svix-signature')!, 23 | }) 24 | } catch (error) { 25 | const logger = getLogger('webhookAuthMiddleware') 26 | logger.error(error) 27 | 28 | return c.json({ success: false, message: `svix validation failed` }, 400) 29 | } 30 | 31 | return await next() 32 | } 33 | 34 | if (path.includes('revenuecat')) { 35 | const { REVENUECAT_WEBHOOK_SECRET } = process.env 36 | 37 | if (!REVENUECAT_WEBHOOK_SECRET) { 38 | return c.json({ message: 'REVENUECAT_WEBHOOK_SECRET is not set' }, 500) 39 | } 40 | 41 | const authorization = c.req.header('Authorization') 42 | 43 | if ( 44 | !authorization || 45 | authorization !== `Bearer ${REVENUECAT_WEBHOOK_SECRET}` 46 | ) { 47 | return c.json({ success: false, message: 'unauthorized' }, 401) 48 | } 49 | 50 | return await next() 51 | } 52 | 53 | return c.json({ success: false, message: 'Not found' }, 404) 54 | }) 55 | -------------------------------------------------------------------------------- /apps/api/v1/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import { getAuth } from '@hono/clerk-auth' 2 | import { Hono } from 'hono' 3 | import { findUserById } from '../services/user.service' 4 | 5 | const router = new Hono().get('/me', async (c) => { 6 | const auth = getAuth(c) 7 | 8 | if (!auth?.userId) { 9 | return c.json({ message: 'unauthorized' }, 401) 10 | } 11 | 12 | const user = await findUserById(auth.userId) 13 | 14 | if (!user) { 15 | return c.json({ message: 'unauthorized' }, 401) 16 | } 17 | 18 | return c.json(user) 19 | }) 20 | 21 | export default router 22 | -------------------------------------------------------------------------------- /apps/api/v1/routes/exchange-rates.ts: -------------------------------------------------------------------------------- 1 | import { zValidator } from '@hono/zod-validator' 2 | import { Hono } from 'hono' 3 | import { z } from 'zod' 4 | import { 5 | getExchangeRate, 6 | getExchangeRates, 7 | } from '../services/exchange-rates.service' 8 | 9 | const router = new Hono() 10 | // TODO: Enable this later 11 | // .use(async (c, next) => { 12 | // const apiKey = c.req.header('x-api-key') 13 | 14 | // if (!apiKey || apiKey !== process.env.API_SECRET_KEY) { 15 | // return c.json({ message: 'Unauthorized' }, 401) 16 | // } 17 | 18 | // await next() 19 | // }) 20 | 21 | .get( 22 | '/', 23 | zValidator( 24 | 'query', 25 | z.object({ 26 | date: z.string().default('latest'), 27 | }), 28 | ), 29 | async (c) => { 30 | const { date } = c.req.valid('query') 31 | 32 | const exchangeRates = await getExchangeRates({ date }) 33 | 34 | return c.json(exchangeRates) 35 | }, 36 | ) 37 | 38 | .get( 39 | '/:fromCurrency/:toCurrency', 40 | zValidator( 41 | 'query', 42 | z.object({ 43 | date: z.string().default('latest'), 44 | }), 45 | ), 46 | zValidator( 47 | 'param', 48 | z.object({ 49 | fromCurrency: z.string(), 50 | toCurrency: z.string(), 51 | }), 52 | ), 53 | async (c) => { 54 | const { fromCurrency, toCurrency } = c.req.valid('param') 55 | const { date } = c.req.valid('query') 56 | 57 | const exchangeRate = await getExchangeRate({ 58 | date, 59 | fromCurrency: fromCurrency, 60 | toCurrency: toCurrency, 61 | }) 62 | 63 | if (!exchangeRate) { 64 | return c.json({ message: 'Exchange rate not found' }, 404) 65 | } 66 | 67 | return c.json(exchangeRate) 68 | }, 69 | ) 70 | 71 | export default router 72 | -------------------------------------------------------------------------------- /apps/api/v1/routes/utils.ts: -------------------------------------------------------------------------------- 1 | import { zValidator } from '@hono/zod-validator' 2 | import { z } from 'zod' 3 | 4 | export const zDeviceLanguageHeader = ({ 5 | required = false, 6 | }: { required?: boolean } = {}) => 7 | zValidator( 8 | 'header', 9 | z.object({ 10 | 'x-device-language': required ? z.string() : z.string().optional(), 11 | }), 12 | ) 13 | 14 | export const zDeviceCurrencyHeader = ({ 15 | required = false, 16 | }: { required?: boolean } = {}) => 17 | zValidator( 18 | 'header', 19 | z.object({ 20 | 'x-device-currency': required ? z.string() : z.string().optional(), 21 | }), 22 | ) 23 | -------------------------------------------------------------------------------- /apps/api/v1/routes/webhooks/clerk.ts: -------------------------------------------------------------------------------- 1 | import { zValidator } from '@hono/zod-validator' 2 | import { Hono } from 'hono' 3 | import { z } from 'zod' 4 | import { webhookAuthMiddleware } from '../../middlewares/webhook-auth' 5 | import { deleteUser } from '../../services/user.service' 6 | 7 | const zClerkUserData = z.object({ id: z.string() }) // Define more fields if needed 8 | 9 | const zPayload = z.discriminatedUnion('type', [ 10 | z.object({ 11 | type: z.literal('user.deleted'), 12 | data: zClerkUserData, 13 | }), 14 | ]) 15 | 16 | const router = new Hono() 17 | .use(webhookAuthMiddleware) 18 | .post('/', zValidator('json', zPayload), async (c) => { 19 | const payload = c.req.valid('json') 20 | 21 | switch (payload.type) { 22 | case 'user.deleted': 23 | await deleteUser(payload.data.id) 24 | return c.json({ success: true, message: 'user deleted' }) 25 | 26 | default: 27 | return c.json( 28 | { success: false, message: `${payload.type} is not supported` }, 29 | 400, 30 | ) 31 | } 32 | }) 33 | 34 | export default router 35 | -------------------------------------------------------------------------------- /apps/api/v1/routes/webhooks/revenuecat.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This webhook route is used to sync user subscription data from RevenueCat. 3 | The webhook only uses the user's ID to find the user and sync their subscription data. 4 | */ 5 | 6 | import { zValidator } from '@hono/zod-validator' 7 | import { Hono } from 'hono' 8 | import { z } from 'zod' 9 | import { getLogger } from '../../../lib/log' 10 | import { webhookAuthMiddleware } from '../../middlewares/webhook-auth' 11 | import { findUserById, syncUserSubscription } from '../../services/user.service' 12 | 13 | const zPayload = z.object({ 14 | api_version: z.literal('1.0'), 15 | event: z.object({ 16 | app_user_id: z.string(), 17 | environment: z.enum(['SANDBOX', 'PRODUCTION']), 18 | original_app_user_id: z.string(), 19 | }), 20 | }) 21 | 22 | const router = new Hono() 23 | .use(webhookAuthMiddleware) 24 | .post('/', zValidator('json', zPayload), async (c) => { 25 | const logger = getLogger('webhooks:revenuecat') 26 | const payload = c.req.valid('json') 27 | const { event } = payload 28 | 29 | logger.debug('Received payload %o', payload) 30 | 31 | const userId = event.original_app_user_id || event.app_user_id 32 | const user = userId ? await findUserById(userId) : null 33 | 34 | if (!user) { 35 | logger.warn('User not found for id %s', userId) 36 | return c.json({ success: false, message: 'user not found' }, 404) 37 | } 38 | 39 | const syncedUser = await syncUserSubscription(user) 40 | 41 | logger.debug('Synced user %o', syncedUser) 42 | 43 | return c.json({ success: true }) 44 | }) 45 | 46 | export default router 47 | -------------------------------------------------------------------------------- /apps/api/v1/services/ai-cache.service.ts: -------------------------------------------------------------------------------- 1 | import prisma from '../../lib/prisma' 2 | 3 | export async function findAiCacheByQuery({ 4 | query, 5 | }: { 6 | query: string 7 | }) { 8 | return await prisma.cachedGptResponse.findFirst({ 9 | where: { query }, 10 | orderBy: { updatedAt: 'desc' }, 11 | }) 12 | } 13 | 14 | export async function createAiCache({ 15 | query, 16 | response, 17 | }: { 18 | query: string 19 | response: string 20 | }) { 21 | return await prisma.cachedGptResponse.create({ 22 | data: { query, response }, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /apps/api/v1/services/clerk.service.ts: -------------------------------------------------------------------------------- 1 | import { clerkClient } from '@clerk/clerk-sdk-node' 2 | import { getLogger } from '../../lib/log' 3 | 4 | export async function deleteClerkUser(userId: string) { 5 | const logger = getLogger(`clerk.service:${deleteClerkUser.name}:${userId}`) 6 | 7 | try { 8 | const deletedUser = await clerkClient.users.deleteUser(userId) 9 | logger.debug(`Deleted user %o`, deletedUser) 10 | return deletedUser 11 | } catch (error) { 12 | logger.error(`Failed to delete user with id: ${userId}`) 13 | logger.debug(error) 14 | throw error 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/api/v1/services/file.service.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto' 2 | import { getLogger } from '../../lib/log' 3 | 4 | export async function hashFile(file: File) { 5 | const log = getLogger(`file.service:${hashFile.name}`) 6 | 7 | log.debug('Hashing file. File size: %d', file.size) 8 | 9 | const buffer = Buffer.from(await file.arrayBuffer()) 10 | const hash = createHash('sha256').update(buffer).digest('hex') 11 | 12 | log.info('Hashed file. Hash: %s', hash) 13 | 14 | return hash 15 | } 16 | -------------------------------------------------------------------------------- /apps/api/v1/services/revenue-cat.service.ts: -------------------------------------------------------------------------------- 1 | import RevenueCatV1 from '../../lib/revenuecat-v1' 2 | import type { CustomerInfo } from '../../lib/revenuecat-v1.types' 3 | 4 | const { REVENUECAT_API_V1_KEY } = process.env 5 | 6 | const IS_PROD = process.env.VERCEL_ENV === 'production' 7 | const rc = new RevenueCatV1(REVENUECAT_API_V1_KEY) 8 | 9 | export async function getOrCreateCustomer({ 10 | userId, 11 | }: { 12 | userId: string 13 | }): Promise { 14 | const customer = await rc.getOrCreateCustomer(userId, { 15 | 'X-Is-Sandbox': IS_PROD ? 'false' : 'true', 16 | }) 17 | 18 | return customer 19 | } 20 | 21 | export function getCustomerActiveSubscription(customer: CustomerInfo) { 22 | const active = Object.entries(customer.subscriber.entitlements).find( 23 | ([, entitlement]) => new Date(entitlement.expires_date) > new Date(), 24 | ) 25 | 26 | if (!active) { 27 | return null 28 | } 29 | 30 | return { 31 | entitlement: active[0], 32 | subscription: active[1], 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/api/v1/services/user-metadata.service.ts: -------------------------------------------------------------------------------- 1 | import type { User, UserMetadata } from '@prisma/client' 2 | import prisma from '../../lib/prisma' 3 | import { findUserById } from './user.service' 4 | 5 | /** 6 | * Checks if the user can update the user metadata 7 | * @returns true if the user can update the metadata, false otherwise 8 | */ 9 | export function canUserUpdateMetadata({ 10 | user, 11 | metadata, 12 | }: { 13 | /** The user who wants to update metadata */ 14 | user: User 15 | /** The target metadata to update */ 16 | metadata: Pick 17 | }) { 18 | return user.id === metadata.userId 19 | } 20 | 21 | /** 22 | * Updates the user metadata 23 | * @returns the updated user with metadata 24 | */ 25 | export async function updateUserMetadata({ 26 | userId, 27 | metadataId, 28 | data, 29 | }: { 30 | /** The user id who wants to update metadata */ 31 | userId: string 32 | /** The target metadata to update */ 33 | metadataId?: string 34 | /** The data to update */ 35 | data: { timezone: string } 36 | }) { 37 | const metadata = await prisma.userMetadata.upsert({ 38 | where: { 39 | id: metadataId, 40 | }, 41 | create: { 42 | ...data, 43 | userId, 44 | }, 45 | update: data, 46 | }) 47 | 48 | return findUserById(metadata.userId) 49 | } 50 | -------------------------------------------------------------------------------- /apps/api/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "crons": [ 3 | { 4 | "path": "/api/cron/users/sync-subscriptions", 5 | "schedule": "0 * * * *" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/mobile/.env.example: -------------------------------------------------------------------------------- 1 | EXPO_USE_METRO_WORKSPACE_ROOT=1 2 | EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY= 3 | EXPO_PUBLIC_API_URL= 4 | SENTRY_ORG= 5 | SENTRY_PROJECT= 6 | SENTRY_AUTH_TOKEN= 7 | EXPO_PUBLIC_SENTRY_DSN= 8 | EXPO_PUBLIC_POSTHOG_API_KEY= 9 | EXPO_PUBLIC_POSTHOG_HOST= 10 | EXPO_PUBLIC_REVENUECAT_PROJECT_IOS_API_KEY= -------------------------------------------------------------------------------- /apps/mobile/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 17 | # The following patterns were generated by expo-cli 18 | 19 | expo-env.d.ts 20 | # @end expo-cli 21 | 22 | # build artifacts 23 | *.tar.gz -------------------------------------------------------------------------------- /apps/mobile/README.md: -------------------------------------------------------------------------------- 1 | # @6pm/mobile 2 | 3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | 2. Start the app 14 | 15 | ```bash 16 | npx expo start 17 | ``` 18 | 19 | In the output, you'll find options to open the app in a 20 | 21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/) 22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo 25 | 26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). 27 | 28 | ## Get a fresh project 29 | 30 | When you're ready, run: 31 | 32 | ```bash 33 | npm run reset-project 34 | ``` 35 | 36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. 37 | 38 | ## Learn more 39 | 40 | To learn more about developing your project with Expo, look at the following resources: 41 | 42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). 43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. 44 | 45 | ## Join the community 46 | 47 | Join our community of developers creating universal apps. 48 | 49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. 50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. 51 | -------------------------------------------------------------------------------- /apps/mobile/android/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Android/IntelliJ 6 | # 7 | build/ 8 | .idea 9 | .gradle 10 | local.properties 11 | *.iml 12 | *.hprof 13 | .cxx/ 14 | 15 | # Bundle artifacts 16 | *.jsbundle 17 | -------------------------------------------------------------------------------- /apps/mobile/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/debug.keystore -------------------------------------------------------------------------------- /apps/mobile/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # react-native-reanimated 11 | -keep class com.swmansion.reanimated.** { *; } 12 | -keep class com.facebook.react.turbomodule.** { *; } 13 | 14 | # Add any project specific keep options here: 15 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_image.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_image.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable/splashscreen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-hdpi/dark.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/digital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-hdpi/digital.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-hdpi/light.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-hdpi/original.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-mdpi/dark.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/digital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-mdpi/digital.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-mdpi/light.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-mdpi/original.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xhdpi/dark.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/digital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xhdpi/digital.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xhdpi/light.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xhdpi/original.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/dark.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/digital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/digital.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/light.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/original.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/dark.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/digital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/digital.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/light.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/original.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #000000 3 | #000000 4 | #023c69 5 | #000000 6 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6pm 3 | contain 4 | false 5 | automatic 6 | 1.0.2 7 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 17 | -------------------------------------------------------------------------------- /apps/mobile/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /apps/mobile/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /apps/mobile/android/react-settings-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") version "1.9.24" 5 | id("java-gradle-plugin") 6 | } 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | gradlePlugin { 13 | plugins { 14 | create("reactSettingsPlugin") { 15 | id = "com.facebook.react.settings" 16 | implementationClass = "expo.plugins.ReactSettingsPlugin" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/mobile/android/react-settings-plugin/src/main/kotlin/expo/plugins/ReactSettingsPlugin.kt: -------------------------------------------------------------------------------- 1 | package expo.plugins 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.initialization.Settings 5 | 6 | class ReactSettingsPlugin : Plugin { 7 | override fun apply(settings: Settings) { 8 | // Do nothing, just register the plugin. 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/mobile/android/sentry.properties: -------------------------------------------------------------------------------- 1 | defaults.url=https://sentry.io/ 2 | defaults.org=get6pm 3 | defaults.project=6pm-mobile 4 | # Using SENTRY_AUTH_TOKEN environment variable -------------------------------------------------------------------------------- /apps/mobile/app/(app)/blob-viewer.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalSearchParams } from 'expo-router' 2 | import { Image, View } from 'react-native' 3 | 4 | export default function BlobViewerScreen() { 5 | const { blobObjectUrl } = useLocalSearchParams() 6 | if (!blobObjectUrl) { 7 | return null 8 | } 9 | return ( 10 | 11 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /apps/mobile/app/(app)/budget/new-budget.tsx: -------------------------------------------------------------------------------- 1 | import { BudgetForm } from '@/components/budget/budget-form' 2 | import { useUserMetadata } from '@/hooks/use-user-metadata' 3 | import { useCreateBudget } from '@/stores/budget/hooks' 4 | import type { BudgetFormValues } from '@6pm/validation' 5 | import { createId } from '@paralleldrive/cuid2' 6 | import { PortalHost, useModalPortalRoot } from '@rn-primitives/portal' 7 | import { useRouter } from 'expo-router' 8 | import { View } from 'react-native' 9 | 10 | export default function CreateBudgetScreen() { 11 | const router = useRouter() 12 | const { mutateAsync } = useCreateBudget() 13 | const { sideOffset, ...rootProps } = useModalPortalRoot() 14 | const { setDefaultBudgetId } = useUserMetadata() 15 | 16 | const handleCreate = async ({ isDefault, ...data }: BudgetFormValues) => { 17 | const budgetId = createId() 18 | 19 | if (isDefault) { 20 | await setDefaultBudgetId(budgetId) 21 | } 22 | 23 | mutateAsync({ 24 | data: { 25 | ...data, 26 | period: { 27 | ...data.period, 28 | id: createId(), 29 | }, 30 | }, 31 | id: budgetId, 32 | }).catch(() => { 33 | // ignore 34 | }) 35 | router.back() 36 | } 37 | 38 | return ( 39 | 40 | 41 | 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /apps/mobile/app/(app)/category/new-category.tsx: -------------------------------------------------------------------------------- 1 | import { CategoryForm } from '@/components/category/category-form' 2 | import { useCreateCategory } from '@/stores/category/hooks' 3 | import type { CategoryFormValues, CategoryTypeType } from '@6pm/validation' 4 | import { createId } from '@paralleldrive/cuid2' 5 | import { useLocalSearchParams, useRouter } from 'expo-router' 6 | import { View } from 'react-native' 7 | 8 | export default function CreateCategoryScreen() { 9 | const router = useRouter() 10 | const { type = 'EXPENSE' } = useLocalSearchParams<{ 11 | type?: CategoryTypeType 12 | }>() 13 | const { mutateAsync } = useCreateCategory() 14 | 15 | const handleCreate = async (data: CategoryFormValues) => { 16 | mutateAsync({ data, id: createId() }).catch(() => { 17 | // ignore 18 | }) 19 | router.back() 20 | } 21 | 22 | return ( 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /apps/mobile/app/(app)/language.tsx: -------------------------------------------------------------------------------- 1 | import { MenuItem } from '@/components/common/menu-item' 2 | import { useLocale } from '@/locales/provider' 3 | import { t } from '@lingui/macro' 4 | import { useLingui } from '@lingui/react' 5 | import { useRouter } from 'expo-router' 6 | import { CheckIcon } from 'lucide-react-native' 7 | import { ScrollView } from 'react-native' 8 | 9 | export default function LanguageScreen() { 10 | const { i18n } = useLingui() 11 | const { language, setLanguage } = useLocale() 12 | const router = useRouter() 13 | 14 | return ( 15 | 16 | 21 | ) 22 | } 23 | onPress={() => { 24 | setLanguage('en') 25 | router.back() 26 | }} 27 | /> 28 | 33 | ) 34 | } 35 | onPress={() => { 36 | setLanguage('vi') 37 | router.back() 38 | }} 39 | /> 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /apps/mobile/app/(app)/review-transactions.tsx: -------------------------------------------------------------------------------- 1 | import { DraftTransactionItem } from '@/components/transaction/draft-transaction-item' 2 | import { Text } from '@/components/ui/text' 3 | import { useTransactionStore } from '@/stores/transaction/store' 4 | import { t } from '@lingui/macro' 5 | import { useLingui } from '@lingui/react' 6 | import { FlatList } from 'react-native' 7 | 8 | export default function ReviewTransactionsScreen() { 9 | const { draftTransactions } = useTransactionStore() 10 | const { i18n } = useLingui() 11 | 12 | return ( 13 | ( 17 | 18 | )} 19 | keyExtractor={(item) => item.id} 20 | ListEmptyComponent={ 21 | 22 | {t(i18n)`Your pending AI transactions will show up here`} 23 | 24 | } 25 | /> 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /apps/mobile/app/(app)/wallet/new-account.tsx: -------------------------------------------------------------------------------- 1 | import { AccountForm } from '@/components/wallet/account-form' 2 | import { useCreateWallet } from '@/stores/wallet/hooks' 3 | import { WalletBalanceState, type WalletFormValues } from '@6pm/validation' 4 | import { createId } from '@paralleldrive/cuid2' 5 | import { PortalHost, useModalPortalRoot } from '@rn-primitives/portal' 6 | import { useRouter } from 'expo-router' 7 | import { Alert, ScrollView, View } from 'react-native' 8 | 9 | export default function NewAccountScreen() { 10 | const { sideOffset, ...rootProps } = useModalPortalRoot() 11 | const router = useRouter() 12 | const { mutateAsync: mutateCreate } = useCreateWallet() 13 | 14 | const handleCreate = async ({ balance, ...data }: WalletFormValues) => { 15 | const statedBalance = 16 | data.balanceState === WalletBalanceState.Positive 17 | ? balance 18 | : (balance ?? 0) * -1 19 | 20 | mutateCreate({ 21 | id: createId(), 22 | data: { 23 | balance: statedBalance, 24 | ...data, 25 | }, 26 | }).catch((error) => { 27 | Alert.alert(error.message) 28 | }) 29 | 30 | router.back() 31 | } 32 | 33 | return ( 34 | 35 | 41 | 42 | 43 | 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /apps/mobile/app/(auth)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { useAuth } from '@clerk/clerk-expo' 2 | import { Redirect, Stack } from 'expo-router' 3 | import { SafeAreaView } from 'react-native' 4 | 5 | export default function UnAuthenticatedLayout() { 6 | const { isSignedIn } = useAuth() 7 | 8 | if (isSignedIn) { 9 | return 10 | } 11 | 12 | return ( 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /apps/mobile/app/(aux)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { BackButton } from '@/components/common/back-button' 2 | import { useColorPalette } from '@/hooks/use-color-palette' 3 | import { t } from '@lingui/macro' 4 | import { useLingui } from '@lingui/react' 5 | import { Stack } from 'expo-router' 6 | import { SafeAreaView } from 'react-native' 7 | 8 | export default function AuxiliaryLayout() { 9 | const { getColor } = useColorPalette() 10 | 11 | const { i18n } = useLingui() 12 | return ( 13 | 14 | , 29 | }} 30 | > 31 | 38 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /apps/mobile/app/(aux)/terms-of-service.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native' 2 | 3 | export default function TermsScreen() { 4 | return ( 5 | 6 | lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 7 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 8 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 9 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 10 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat 11 | cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id 12 | est laborum. 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /apps/mobile/app/+html.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollViewStyleReset } from 'expo-router/html' 2 | import type { PropsWithChildren } from 'react' 3 | 4 | /** 5 | * This file is web-only and used to configure the root HTML for every web page during static rendering. 6 | * The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs. 7 | */ 8 | export default function Root({ children }: PropsWithChildren) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 18 | 19 | {/* 20 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 21 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 22 | */} 23 | 24 | 25 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 26 | {/* biome-ignore lint/style/useNamingConvention: */} 27 | {/* biome-ignore lint/security/noDangerouslySetInnerHtml: */} 28 |