├── .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 |
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 |
29 | {/* Add any additional elements that you want globally available on web... */}
30 |
31 | {children}
32 |
33 | )
34 | }
35 |
36 | const responsiveBackground = `
37 | body {
38 | background-color: #fff;
39 | }
40 | @media (prefers-color-scheme: dark) {
41 | body {
42 | background-color: #000;
43 | }
44 | }`
45 |
--------------------------------------------------------------------------------
/apps/mobile/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Link, Stack, useRouter } from 'expo-router'
2 | import { View } from 'react-native'
3 |
4 | import { Button } from '@/components/ui/button'
5 | import { Text } from '@/components/ui/text'
6 |
7 | export default function NotFoundScreen() {
8 | const router = useRouter()
9 | return (
10 | <>
11 |
12 |
13 | This screen doesn't exist.
14 |
15 |
18 |
19 |
22 |
23 |
24 |
25 | >
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/apps/mobile/app/onboarding/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { BackButton } from '@/components/common/back-button'
2 | import { useColorPalette } from '@/hooks/use-color-palette'
3 | import { Stack } from 'expo-router'
4 |
5 | export default function OnboardingLayout() {
6 | const { getColor } = useColorPalette()
7 |
8 | return (
9 | ,
16 | headerTitle: '',
17 | }}
18 | >
19 | null,
23 | }}
24 | />
25 |
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/apps/mobile/app/onboarding/step-one.tsx:
--------------------------------------------------------------------------------
1 | import { OnboardIllustration } from '@/components/svg-assets/onboard-illustration'
2 | import { Button } from '@/components/ui/button'
3 | import { Text } from '@/components/ui/text'
4 | import { t } from '@lingui/macro'
5 | import { useLingui } from '@lingui/react'
6 | import { Link } from 'expo-router'
7 | import { ArrowRightIcon } from 'lucide-react-native'
8 | import { ScrollView, View } from 'react-native'
9 | import { useSafeAreaInsets } from 'react-native-safe-area-context'
10 |
11 | export default function StepOneScreen() {
12 | const { i18n } = useLingui()
13 | const { bottom } = useSafeAreaInsets()
14 |
15 | return (
16 |
23 |
24 |
25 | {t(i18n)`Welcome to 6pm!`}
26 |
27 |
28 | {t(i18n)`Get started by setting your monthly budget.`}
29 |
30 |
31 |
32 |
33 |
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/apps/mobile/app/onboarding/step-three.tsx:
--------------------------------------------------------------------------------
1 | import { NotificationIllustration } from '@/components/svg-assets/notification-illustration'
2 | import { Button } from '@/components/ui/button'
3 | import { Text } from '@/components/ui/text'
4 | import { useUserSettingsStore } from '@/stores/user-settings/store'
5 | import { t } from '@lingui/macro'
6 | import { useLingui } from '@lingui/react'
7 | import * as Notifications from 'expo-notifications'
8 | import { useRouter } from 'expo-router'
9 | import { ScrollView, View } from 'react-native'
10 | import { useSafeAreaInsets } from 'react-native-safe-area-context'
11 |
12 | export default function StepThreeScreen() {
13 | const { i18n } = useLingui()
14 | const { bottom } = useSafeAreaInsets()
15 | const router = useRouter()
16 | const { setEnabledPushNotifications } = useUserSettingsStore()
17 |
18 | async function handleEnableNotification() {
19 | const { status } = await Notifications.requestPermissionsAsync()
20 | if (status === 'granted') {
21 | setEnabledPushNotifications(true)
22 | }
23 | router.replace('/')
24 | }
25 |
26 | return (
27 |
34 |
35 |
36 | {t(i18n)`Enable spending alerts`}
37 |
38 |
39 | {t(i18n)`Keeping up with your spending and budgets.`}
40 |
41 |
42 |
43 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/apps/mobile/assets/fonts/Haskoy-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/fonts/Haskoy-Bold.ttf
--------------------------------------------------------------------------------
/apps/mobile/assets/fonts/Haskoy-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/fonts/Haskoy-Medium.ttf
--------------------------------------------------------------------------------
/apps/mobile/assets/fonts/Haskoy-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/fonts/Haskoy-Regular.ttf
--------------------------------------------------------------------------------
/apps/mobile/assets/fonts/Haskoy-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/fonts/Haskoy-SemiBold.ttf
--------------------------------------------------------------------------------
/apps/mobile/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/app-icons/dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/app-icons/dark.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/app-icons/digital.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/app-icons/digital.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/app-icons/light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/app-icons/light.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/app-icons/original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/app-icons/original.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/favicon.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/icon.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/paywall-images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/paywall-images/1.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/paywall-images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/paywall-images/2.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/paywall-images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/paywall-images/3.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/paywall-images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/paywall-images/4.png
--------------------------------------------------------------------------------
/apps/mobile/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/get6pm/6pm/49362109bc34b540c27dee5d4ff05c0475b75b21/apps/mobile/assets/images/splash.png
--------------------------------------------------------------------------------
/apps/mobile/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true)
3 | return {
4 | presets: [
5 | ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
6 | 'nativewind/babel',
7 | ],
8 | plugins: ['macros'],
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/mobile/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
3 | "extends": ["../../biome.json"],
4 | "files": {
5 | "include": ["**/*.ts", "**/*.tsx"],
6 | "ignore": ["**/*.d.ts"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/mobile/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "platforms": "universal",
3 | "aliases": {
4 | "components": "@/components",
5 | "lib": "@/lib"
6 | }
7 | }
--------------------------------------------------------------------------------
/apps/mobile/components/__tests__/ThemedText-test.tsx:
--------------------------------------------------------------------------------
1 | import renderer from 'react-test-renderer'
2 |
3 | import { ThemedText } from '../ThemedText'
4 |
5 | it(`renders correctly`, () => {
6 | const tree = renderer.create(Snapshot test!).toJSON()
7 |
8 | expect(tree).toMatchSnapshot()
9 | })
10 |
--------------------------------------------------------------------------------
/apps/mobile/components/__tests__/__snapshots__/ThemedText-test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders correctly 1`] = `
4 |
22 | Snapshot test!
23 |
24 | `;
25 |
--------------------------------------------------------------------------------
/apps/mobile/components/auth/auth-local.tsx:
--------------------------------------------------------------------------------
1 | import { t } from '@lingui/macro'
2 | import { useLingui } from '@lingui/react'
3 | import * as LocalAuthentication from 'expo-local-authentication'
4 | import { LockKeyholeIcon, ScanFaceIcon } from 'lucide-react-native'
5 | import { useCallback, useEffect } from 'react'
6 | import { SafeAreaView } from 'react-native'
7 | import { Button } from '../ui/button'
8 | import { Text } from '../ui/text'
9 |
10 | type AuthLocalProps = {
11 | onAuthenticated?: () => void
12 | }
13 |
14 | export function AuthLocal({ onAuthenticated }: AuthLocalProps) {
15 | const { i18n } = useLingui()
16 |
17 | const handleAuthenticate = useCallback(async () => {
18 | const result = await LocalAuthentication.authenticateAsync({
19 | // disableDeviceFallback: true,
20 | })
21 | if (result.success) {
22 | onAuthenticated?.()
23 | }
24 | }, [onAuthenticated])
25 |
26 | // biome-ignore lint/correctness/useExhaustiveDependencies:
27 | useEffect(() => {
28 | handleAuthenticate()
29 | }, [])
30 |
31 | return (
32 |
33 |
34 | {t(
35 | i18n,
36 | )`App is locked. Please authenticate to continue.`}
37 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/apps/mobile/components/auth/emailSchema.tsx:
--------------------------------------------------------------------------------
1 | import * as z from 'zod'
2 |
3 | export const emailFormSchema = z.object({
4 | emailAddress: z
5 | .string()
6 | .email()
7 | .min(1, { message: 'Input your email address' }),
8 | })
9 |
10 | export type EmailFormValues = z.infer
11 |
12 | export const verifyEmailFormSchema = z.object({
13 | code: z.string().min(1, { message: 'Input the verification code' }),
14 | })
15 |
16 | export type VerifyEmailFormValues = z.infer
17 |
--------------------------------------------------------------------------------
/apps/mobile/components/budget/budget-statistic.tsx:
--------------------------------------------------------------------------------
1 | import { t } from '@lingui/macro'
2 | import { useLingui } from '@lingui/react'
3 | import { View } from 'react-native'
4 | import { AmountFormat } from '../common/amount-format'
5 | import { Text } from '../ui/text'
6 |
7 | type BudgetStatisticProps = {
8 | totalRemaining: number
9 | remainingPerDay: number
10 | }
11 |
12 | export function BudgetStatistic({
13 | totalRemaining,
14 | remainingPerDay,
15 | }: BudgetStatisticProps) {
16 | const { i18n } = useLingui()
17 |
18 | return (
19 |
20 |
21 |
22 |
23 | {t(i18n)`Left this month`}
24 |
25 |
26 |
27 |
28 |
29 | {t(i18n)`Left per day`}
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/apps/mobile/components/budget/period-control.tsx:
--------------------------------------------------------------------------------
1 | import { formatDateRange } from '@/lib/date'
2 | import { dayjsExtended } from '@6pm/utilities'
3 | import type { BudgetPeriodConfig } from '@6pm/validation'
4 | import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react-native'
5 | import { View } from 'react-native'
6 | import { Button } from '../ui/button'
7 | import { Text } from '../ui/text'
8 |
9 | export type PeriodControlProps = {
10 | periodConfigs: BudgetPeriodConfig[]
11 | index: number
12 | onChange: (index: number) => void
13 | }
14 |
15 | export function PeriodControl({
16 | periodConfigs,
17 | index = 0,
18 | onChange,
19 | }: PeriodControlProps) {
20 | const couldGoBack = index > 0
21 | const couldGoForward = index < periodConfigs.length - 1
22 | const timezoneOffset = new Date().getTimezoneOffset()
23 |
24 | return (
25 |
26 |
34 |
35 | {formatDateRange(
36 | dayjsExtended(periodConfigs[index].startDate!)
37 | .add(timezoneOffset, 'm')
38 | .toDate(),
39 | dayjsExtended(periodConfigs[index].endDate!)
40 | .add(timezoneOffset, 'm')
41 | .toDate(),
42 | )}
43 |
44 |
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/apps/mobile/components/budget/period-range-field.tsx:
--------------------------------------------------------------------------------
1 | import { useController, useFormState, useWatch } from 'react-hook-form'
2 | import { View } from 'react-native'
3 | import { DateRangePicker } from '../common/date-range-picker'
4 | import { Text } from '../ui/text'
5 |
6 | export function PeriodRangeField() {
7 | const { errors } = useFormState()
8 | const periodType = useWatch({ name: 'period.type' })
9 |
10 | const {
11 | field: { onChange: onChangeStartDate, value: startDate },
12 | } = useController({
13 | name: 'period.startDate',
14 | })
15 | const {
16 | field: { onChange: onChangeEndDate, value: endDate },
17 | } = useController({
18 | name: 'period.endDate',
19 | })
20 |
21 | if (periodType !== 'CUSTOM') {
22 | return null
23 | }
24 |
25 | return (
26 |
27 | {
30 | const [startDate, endDate] = dates ?? []
31 | onChangeStartDate(startDate)
32 | onChangeEndDate(endDate)
33 | }}
34 | />
35 | {!!errors.period?.root?.message && (
36 |
37 | {errors.period.root.message.toString()}
38 |
39 | )}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/apps/mobile/components/category/category-item.tsx:
--------------------------------------------------------------------------------
1 | import type { Category } from '@6pm/validation'
2 | import { Link } from 'expo-router'
3 | import type { FC } from 'react'
4 | import GenericIcon from '../common/generic-icon'
5 | import { MenuItem } from '../common/menu-item'
6 |
7 | type CategoryItemProps = {
8 | category: Category
9 | }
10 |
11 | export const CategoryItem: FC = ({ category }) => {
12 | return (
13 |
21 |