├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values-night │ │ │ │ │ └── colors.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── 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 │ │ │ │ │ ├── splashscreen.xml │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── vladi │ │ │ │ └── ExpenseZen │ │ │ │ ├── MainApplication.java │ │ │ │ └── MainActivity.java │ │ ├── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── vladi │ │ │ │ └── ExpenseZen │ │ │ │ └── ReactNativeFlipper.java │ │ └── release │ │ │ └── java │ │ │ └── com │ │ │ └── expensezen │ │ │ └── ReactNativeFlipper.java │ ├── debug.keystore │ └── proguard-rules.pro ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── build.gradle ├── gradle.properties └── gradlew.bat ├── constants ├── Sizes.ts ├── NavigationConstants.ts ├── Tables.ts ├── Months.ts └── PostgresFunctions.ts ├── .vscode └── settings.json ├── assets ├── icon.png ├── favicon.png ├── splash.png ├── adaptive-icon.png ├── fonts │ ├── SourceSansProBold.ttf │ └── SourceSansProRegular.ttf └── SVG │ ├── Facebook.svg │ ├── index.ts │ ├── Google.svg │ ├── NoData.svg │ ├── NoChartData.svg │ ├── SetBudget.svg │ ├── Savings.svg │ └── Progress.svg ├── screenshots ├── budgets_screen.jpg ├── pie_screen_dark.jpg ├── register_screen.jpg ├── categories_screen.jpg ├── home_screen_dark.jpg ├── home_screen_light.jpg ├── pie_screen_light.jpg ├── add_expense_screen.jpg ├── login_screen_light.jpg ├── onboarding_screen_1.jpg ├── onboarding_screen_2.jpg ├── change_password_screen.jpg ├── settings_screen_light.jpg └── category_expenses_screen.jpg ├── interfaces ├── Provider.ts ├── GraphCategory.ts ├── Month.ts ├── Budget.ts ├── Step.ts ├── Category.ts ├── Expense.ts ├── User.ts └── Navigation.ts ├── tsconfig.json ├── .prettierrc ├── utils ├── getCurrentDate.ts ├── compareHashed.ts ├── hashPassword.ts └── getCategoryIcon.js ├── commonStyles.ts ├── env.d.ts ├── schemas ├── expenseSchema.ts ├── loginSchema.ts ├── changePasswordSchemta.ts ├── resetPasswordScheama.ts └── registerSchema.ts ├── index.js ├── api ├── migrations │ ├── get_today_total.sql │ ├── convert_expenses_currency.sql │ ├── convert_budgets_currency.sql │ ├── get_month_total.sql │ ├── get_user_budgets.sql │ ├── get_month_expenses.sql │ ├── save_user_budgets.sql │ └── get_top_spendings.sql ├── supabase.ts └── services │ ├── CategoryService.ts │ ├── CurrencyService.ts │ ├── ExpenseService.ts │ └── UserService.ts ├── components ├── shared │ ├── EzHeaderTitle.tsx │ ├── EZHeaderBackground.tsx │ ├── EZButton.tsx │ ├── EZProgress.tsx │ └── EZInput.tsx ├── AppContainer.tsx ├── OnboardingStep.tsx ├── Paginator.tsx ├── MonthlyBudgetItem.tsx ├── TopSpendingCategory.tsx ├── CategoryItem.tsx ├── GraphCategoryItem.tsx ├── MonthlyBudgetCategory.tsx ├── LoginProviders.tsx ├── ChangePasswordForm.tsx ├── ResetPasswordForm.tsx ├── LoginForm.tsx └── RegisterForm.tsx ├── babel.config.js ├── redux ├── onboardReducer.ts ├── store.ts ├── userReducer.ts └── expensesReducers.ts ├── metro.config.js ├── app.json ├── screens ├── ChangePasswordScreen.tsx ├── ResetPasswordScreen.tsx ├── CategoriesScreen.tsx ├── RegisterScreen.tsx ├── LoginScreen.tsx ├── AboutScreen.tsx ├── AddCurrencyScreen.tsx ├── CategoryExpensesScreen.tsx ├── EditBudgetsScreen.tsx ├── OnboardingScreen.tsx ├── GraphScreen.tsx └── AddExpenseScreen.tsx ├── navigation ├── Navigation.tsx ├── StackNavigator.tsx └── TabNavigator.tsx ├── colors.ts ├── .gitignore ├── README.md ├── App.tsx └── package.json /android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /constants/Sizes.ts: -------------------------------------------------------------------------------- 1 | export const MONTH_ITEM_WIDTH = 120; 2 | -------------------------------------------------------------------------------- /constants/NavigationConstants.ts: -------------------------------------------------------------------------------- 1 | export const TAB_BAR_HEIGHT = 80; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/assets/splash.png -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /screenshots/budgets_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/budgets_screen.jpg -------------------------------------------------------------------------------- /screenshots/pie_screen_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/pie_screen_dark.jpg -------------------------------------------------------------------------------- /screenshots/register_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/register_screen.jpg -------------------------------------------------------------------------------- /screenshots/categories_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/categories_screen.jpg -------------------------------------------------------------------------------- /screenshots/home_screen_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/home_screen_dark.jpg -------------------------------------------------------------------------------- /screenshots/home_screen_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/home_screen_light.jpg -------------------------------------------------------------------------------- /screenshots/pie_screen_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/pie_screen_light.jpg -------------------------------------------------------------------------------- /assets/fonts/SourceSansProBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/assets/fonts/SourceSansProBold.ttf -------------------------------------------------------------------------------- /interfaces/Provider.ts: -------------------------------------------------------------------------------- 1 | export enum Provider { 2 | DIRECT = "Direct", 3 | GOOGLE = "Google", 4 | FACEBOOK = "Facebook", 5 | } 6 | -------------------------------------------------------------------------------- /screenshots/add_expense_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/add_expense_screen.jpg -------------------------------------------------------------------------------- /screenshots/login_screen_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/login_screen_light.jpg -------------------------------------------------------------------------------- /screenshots/onboarding_screen_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/onboarding_screen_1.jpg -------------------------------------------------------------------------------- /screenshots/onboarding_screen_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/onboarding_screen_2.jpg -------------------------------------------------------------------------------- /assets/fonts/SourceSansProRegular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/assets/fonts/SourceSansProRegular.ttf -------------------------------------------------------------------------------- /screenshots/change_password_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/change_password_screen.jpg -------------------------------------------------------------------------------- /screenshots/settings_screen_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/settings_screen_light.jpg -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /screenshots/category_expenses_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/screenshots/category_expenses_screen.jpg -------------------------------------------------------------------------------- /interfaces/GraphCategory.ts: -------------------------------------------------------------------------------- 1 | export interface GraphCategory { 2 | name: string; 3 | amount: number; 4 | color: string; 5 | expenses: number; 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noUnusedLocals": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "bracketSpacing": true, 5 | "bracketSameLine": true, 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /utils/getCurrentDate.ts: -------------------------------------------------------------------------------- 1 | export const getCurrentDate = () => { 2 | const currentDate = new Date().toISOString().slice(0, 10); 3 | return currentDate; 4 | }; 5 | -------------------------------------------------------------------------------- /interfaces/Month.ts: -------------------------------------------------------------------------------- 1 | export type Month = "January" | "February" | "March" |"April" |"May" |"June" |"July" |"August" |"September" |"October" | "November" |"December", 2 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /interfaces/Budget.ts: -------------------------------------------------------------------------------- 1 | export interface Budget { 2 | id?: number; 3 | budget: number; 4 | category?: string; 5 | categoryId?: number; 6 | color?: string; 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/drawable-hdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/drawable-mdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /interfaces/Step.ts: -------------------------------------------------------------------------------- 1 | import { SvgProps } from "react-native-svg"; 2 | 3 | export interface Step { 4 | id: number; 5 | title: string; 6 | description: string; 7 | image: any; 8 | } 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimir-Ciuculescu/ExpenseZen/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splashscreen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 14 | # Bundle artifacts 15 | *.jsbundle 16 | -------------------------------------------------------------------------------- /interfaces/Category.ts: -------------------------------------------------------------------------------- 1 | export interface Category { 2 | id?: number; 3 | name: string; 4 | color?: string; 5 | total?: number; 6 | amount?: number; 7 | icon?: any; 8 | budget?: number; 9 | } 10 | -------------------------------------------------------------------------------- /utils/compareHashed.ts: -------------------------------------------------------------------------------- 1 | import Bcrypt from "react-native-bcrypt"; 2 | 3 | export const compareHashed = async (plainPassword: string, hash: string) => { 4 | return Bcrypt.compareSync(plainPassword, hash); 5 | }; 6 | -------------------------------------------------------------------------------- /commonStyles.ts: -------------------------------------------------------------------------------- 1 | import COLORS from "./colors"; 2 | 3 | export const authInput = { 4 | paddingLeft: 20, 5 | // color: COLORS.PURPLE[700], 6 | fontFamily: "SourceSansPro", 7 | height: 44, 8 | fontSize: 15, 9 | }; 10 | -------------------------------------------------------------------------------- /constants/Tables.ts: -------------------------------------------------------------------------------- 1 | export const CATEGORIES = "categories"; 2 | export const USERS_CURRENCIES = "users_currencies"; 3 | export const USERS = "users"; 4 | export const MONTHLY_BUDGETS = "monthly_budgets"; 5 | export const EXPENSES = "expenses"; 6 | -------------------------------------------------------------------------------- /constants/Months.ts: -------------------------------------------------------------------------------- 1 | export const MONTHS = [ 2 | "January", 3 | "February", 4 | "March", 5 | "April", 6 | "May", 7 | "June", 8 | "July", 9 | "August", 10 | "September", 11 | "October", 12 | "November", 13 | "December", 14 | ]; 15 | -------------------------------------------------------------------------------- /interfaces/Expense.ts: -------------------------------------------------------------------------------- 1 | export interface Expense { 2 | id?: number; 3 | userId?: number; 4 | categoryId?: number; 5 | amount: number; 6 | description?: string; 7 | name?: string; 8 | payDate?: any; 9 | color?: string | undefined; 10 | } 11 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff 3 | #ffffff 4 | #023c69 5 | #ffffff 6 | -------------------------------------------------------------------------------- /interfaces/User.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from "./Provider"; 2 | 3 | export interface User { 4 | id?: number; 5 | firstName: string; 6 | lastName: string; 7 | email: string; 8 | password: string; 9 | repeatPassword?: string; 10 | provider: Provider; 11 | currency?: string; 12 | } 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ExpenseZen 3 | contain 4 | false 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg"; 2 | declare module "*.ttf"; 3 | declare module "@env" { 4 | export const SUPABASE_URL: string; 5 | export const SUPABASE_ANON_KEY: string; 6 | export const HASH_WARNING: string; 7 | export const VIRTUALIZED_WARNING: string; 8 | export const FREECURRENCY_API_KEY: string; 9 | } 10 | -------------------------------------------------------------------------------- /schemas/expenseSchema.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | export const expenseSchema = Yup.object({ 4 | amount: Yup.string().required("Please fill in the amount !"), 5 | category: Yup.string().required("Please select a category !"), 6 | description: Yup.string().required("Please fill in the description !"), 7 | }); 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /schemas/loginSchema.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | export const loginSchema = Yup.object({ 4 | email: Yup.string() 5 | .email("That's not a valid email address !") 6 | .required("Please add you email address"), 7 | password: Yup.string() 8 | .required("Please fill in your password !") 9 | .min(6, "Password must have at least 6 characters"), 10 | }); 11 | -------------------------------------------------------------------------------- /schemas/changePasswordSchemta.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | export const changePasswordSchema = Yup.object({ 4 | password: Yup.string() 5 | .required("Please provide a password") 6 | .min(6, "Password must have at least 6 characters"), 7 | repeatPassword: Yup.string() 8 | .oneOf([Yup.ref("password"), null as any], "Passwords don't match!") 9 | .required("Retype your password "), 10 | }); 11 | -------------------------------------------------------------------------------- /api/migrations/get_today_total.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP function get_today_total 3 | 4 | CREATE OR REPLACE FUNCTION get_today_total(user_id integer) 5 | RETURNS float AS $$ 6 | DECLARE 7 | total float; 8 | BEGIN 9 | SELECT SUM(amount) INTO total FROM expenses WHERE date = CURRENT_DATE AND expenses.user_id = get_today_total.user_id; 10 | 11 | RETURN total; 12 | END; 13 | $$ LANGUAGE plpgsql; 14 | 15 | --SELECT get_today_total(1); -------------------------------------------------------------------------------- /utils/hashPassword.ts: -------------------------------------------------------------------------------- 1 | import Bcrypt from "react-native-bcrypt"; 2 | import isaac from "isaac"; 3 | 4 | Bcrypt.setRandomFallback((len: any): any => { 5 | const buf = new Uint8Array(len); 6 | 7 | return buf.map(() => Math.floor(isaac.random() * 256)); 8 | }); 9 | 10 | export const hashPassword = async (password: string) => { 11 | const hashedPassword = Bcrypt.hashSync(password, Bcrypt.genSaltSync(10)); 12 | 13 | return hashedPassword; 14 | }; 15 | -------------------------------------------------------------------------------- /components/shared/EzHeaderTitle.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "native-base"; 2 | 3 | interface Props { 4 | children: string; 5 | } 6 | 7 | const EZHeaderTitle: React.FC = ({ children }) => { 8 | return ( 9 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default EZHeaderTitle; 20 | -------------------------------------------------------------------------------- /api/migrations/convert_expenses_currency.sql: -------------------------------------------------------------------------------- 1 | DROP function convert_expenses_currency; 2 | 3 | 4 | CREATE OR REPLACE FUNCTION convert_expenses_currency(user_id integer, conversion_rate numeric) 5 | RETURNS VOID AS $$ 6 | BEGIN 7 | UPDATE expenses e 8 | SET amount = amount * convert_expenses_currency.conversion_rate 9 | WHERE e.user_id = convert_expenses_currency.user_id; 10 | 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | 14 | --SELECT convert_expenses_currency(1,2.2) -------------------------------------------------------------------------------- /api/migrations/convert_budgets_currency.sql: -------------------------------------------------------------------------------- 1 | DROP function convert_budgets_currency; 2 | 3 | 4 | CREATE OR REPLACE FUNCTION convert_budgets_currency(user_id integer, conversion_rate numeric) 5 | RETURNS VOID AS $$ 6 | BEGIN 7 | UPDATE monthly_budgets mb 8 | SET budget = budget * convert_budgets_currency.conversion_rate 9 | WHERE mb.user_id = convert_budgets_currency.user_id; 10 | 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | 14 | --SELECT convert_budgets_currency(14,2.2) -------------------------------------------------------------------------------- /assets/SVG/Facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: [ 6 | [ 7 | "module:react-native-dotenv", 8 | { 9 | moduleName: "@env", 10 | path: ".env", 11 | blacklist: null, 12 | whitelist: null, 13 | safe: false, 14 | allowUndefined: true, 15 | }, 16 | ], 17 | "react-native-reanimated/plugin", 18 | ], 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /assets/SVG/index.ts: -------------------------------------------------------------------------------- 1 | import Savings from "./Savings.svg"; 2 | import PieChart from "./PieChart.svg"; 3 | import Progress from "./Progress.svg"; 4 | import Google from "./Google.svg"; 5 | import Facebook from "./Facebook.svg"; 6 | import NoData from "./NoData.svg"; 7 | import SetBudget from "./SetBudget.svg"; 8 | import NoChartData from "./NoChartData.svg"; 9 | 10 | export { 11 | Savings, 12 | PieChart, 13 | Progress, 14 | Google, 15 | Facebook, 16 | NoData, 17 | SetBudget, 18 | NoChartData, 19 | }; 20 | -------------------------------------------------------------------------------- /constants/PostgresFunctions.ts: -------------------------------------------------------------------------------- 1 | export const GET_TOP_SPENDINGS = "get_top_spendings"; 2 | export const GET_USER_BUDGETS = "get_user_budgets"; 3 | export const GET_TODAY_TOTAL = "get_today_total"; 4 | export const GET_MONTH_TOTAL = "get_month_total"; 5 | export const GET_MONTH_EXPENSES = "get_month_expenses"; 6 | export const GET_MONTHLY_CATEGORY_EXPENSES = "get_monthly_category_expenses"; 7 | export const CONVERT_EXPENSES_CURRENCY = "convert_expenses_currency"; 8 | export const CONVERT_BUDGETS_CURRENCY = "convert_budgets_currency"; 9 | -------------------------------------------------------------------------------- /api/migrations/get_month_total.sql: -------------------------------------------------------------------------------- 1 | DROP function get_month_total 2 | 3 | CREATE OR REPLACE FUNCTION get_month_total(start_month date, end_month date,user_id integer) 4 | RETURNS float AS $$ 5 | DECLARE 6 | total float; 7 | BEGIN 8 | SELECT SUM(amount) INTO total FROM expenses WHERE date >= get_month_total.start_month AND date <= get_month_total.end_month AND expenses.user_id = get_month_total.user_id; 9 | 10 | RETURN total; 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | 14 | --SELECT get_month_total('2023-07-01','2023-07-31',1); -------------------------------------------------------------------------------- /schemas/resetPasswordScheama.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | export const resetPasswordSchema = Yup.object({ 4 | email: Yup.string() 5 | .email(`That's not a valid email address`) 6 | .required("Please add your email address !"), 7 | password: Yup.string() 8 | .required("Please provide a password") 9 | .min(6, "Password must have at least 6 characters"), 10 | repeatPassword: Yup.string() 11 | .oneOf([Yup.ref("password"), null as any], "Passwords don't match!") 12 | .required("Retype your password "), 13 | }); 14 | -------------------------------------------------------------------------------- /api/migrations/get_user_budgets.sql: -------------------------------------------------------------------------------- 1 | DROP function get_user_budgets; 2 | 3 | CREATE OR REPLACE FUNCTION get_user_budgets(user_id integer) 4 | RETURNS TABLE (budget float, category text,id integer, color text) as $$ 5 | 6 | BEGIN 7 | return query 8 | select mb.budget, c.name,c.id,c.color from monthly_budgets mb 9 | inner join users u on u.id = mb.user_id 10 | inner join categories c on c.id = mb.category_id 11 | where u.id = get_user_budgets.user_id; 12 | END; 13 | 14 | $$ language plpgsql; 15 | 16 | 17 | --select get_user_budgets(3) 18 | -------------------------------------------------------------------------------- /redux/onboardReducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | interface initialStateProps { 4 | onboarded: boolean; 5 | } 6 | 7 | const initialState: initialStateProps = { 8 | onboarded: false, 9 | }; 10 | 11 | const onboardedReducer = createSlice({ 12 | name: "onboard", 13 | initialState: initialState, 14 | reducers: { 15 | onBoard: (state) => { 16 | state.onboarded = true; 17 | }, 18 | }, 19 | }); 20 | 21 | export const onBoard = onboardedReducer.actions.onBoard; 22 | 23 | export default onboardedReducer.reducer; 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ExpenseZen' 2 | 3 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); 4 | useExpoModules() 5 | 6 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); 7 | applyNativeModulesSettingsGradle(settings) 8 | 9 | include ':app' 10 | includeBuild(new File(["node", "--print", "require.resolve('react-native-gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile()) 11 | -------------------------------------------------------------------------------- /components/shared/EZHeaderBackground.tsx: -------------------------------------------------------------------------------- 1 | import { LinearGradient } from "expo-linear-gradient"; 2 | import { useSelector } from "react-redux"; 3 | import COLORS from "../../colors"; 4 | import { RootState } from "../../redux/store"; 5 | 6 | const EZHeaderBackground: React.FC = () => { 7 | const { theme } = useSelector((state: RootState) => state.user); 8 | 9 | return ( 10 | 16 | ); 17 | }; 18 | 19 | export default EZHeaderBackground; 20 | -------------------------------------------------------------------------------- /schemas/registerSchema.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | export const registerSchema = Yup.object({ 4 | firstName: Yup.string().required("Please add your first name !"), 5 | lastName: Yup.string().required("Please add your second name !"), 6 | email: Yup.string() 7 | .email(`That's not a valid email address !`) 8 | .required("Please add your email address !"), 9 | password: Yup.string() 10 | .required("Please provide a password") 11 | .min(6, "Password must have at least 6 characters"), 12 | repeatPassword: Yup.string() 13 | .oneOf([Yup.ref("password"), null as any], "Passwords don't match!") 14 | .required("Retype your password "), 15 | }); 16 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("metro-config"); 2 | 3 | module.exports = (async () => { 4 | const { 5 | resolver: { sourceExts, assetExts }, 6 | } = await getDefaultConfig(); 7 | return { 8 | transformer: { 9 | getTransformOptions: async () => ({ 10 | transform: { 11 | experimentalImportSupport: false, 12 | inlineRequires: false, 13 | }, 14 | }), 15 | babelTransformerPath: require.resolve("react-native-svg-transformer"), 16 | }, 17 | resolver: { 18 | assetExts: assetExts.filter((ext) => ext !== "svg"), 19 | sourceExts: [...sourceExts, "svg"], 20 | }, 21 | }; 22 | })(); 23 | -------------------------------------------------------------------------------- /api/migrations/get_month_expenses.sql: -------------------------------------------------------------------------------- 1 | DROP function get_month_expenses 2 | 3 | 4 | CREATE OR REPLACE FUNCTION get_month_expenses(start_month date, end_month date, user_id integer) 5 | RETURNS table ("categoryId" integer, name text, description text, amount float, color text, "payDate" date) AS $$ 6 | 7 | BEGIN 8 | return query 9 | SELECT es.category_id AS "categoryId", c.name, es.description, es.amount, c.color, es.date as "payDate" FROM expenses es 10 | INNER join categories c ON c.id = es.category_id 11 | WHERE date >= get_month_expenses.start_month AND date <= get_month_expenses.end_month AND es.user_id = get_month_expenses.user_id; 12 | END; 13 | 14 | $$ LANGUAGE plpgsql; 15 | 16 | 17 | --SELECT get_month_expenses('2023-07-01','2023-07-31',3); -------------------------------------------------------------------------------- /interfaces/Navigation.ts: -------------------------------------------------------------------------------- 1 | import { NativeStackNavigationOptions } from "@react-navigation/native-stack"; 2 | import { Expense } from "./Expense"; 3 | 4 | export type AppStackParamList = { 5 | Onboarding: undefined; 6 | Login: undefined; 7 | Register: undefined; 8 | ResetPassword: undefined; 9 | Tabs: undefined; 10 | Currency: undefined; 11 | CategoryExpenses: { expenses: Expense[]; name: string }; 12 | AddExpense: undefined; 13 | EditBudgets: undefined; 14 | About: undefined; 15 | ChangePassword: undefined; 16 | }; 17 | 18 | export type StackConfig = { 19 | name: keyof AppStackParamList; 20 | component: React.ComponentType; 21 | params?: AppStackParamList[keyof AppStackParamList]; 22 | options?: NativeStackNavigationOptions; 23 | }; 24 | -------------------------------------------------------------------------------- /api/migrations/save_user_budgets.sql: -------------------------------------------------------------------------------- 1 | DROP function save_user_budget; 2 | 3 | CREATE OR REPLACE FUNCTION save_user_budget(user_id integer, category_id integer, budget numeric) 4 | RETURNS void AS $$ 5 | BEGIN 6 | IF EXISTS (SELECT 1 FROM monthly_budgets mb WHERE mb.category_id = save_user_budget.category_id AND mb.user_id = save_user_budget.user_id) THEN 7 | UPDATE monthly_budgets mb 8 | SET budget = save_user_budget.budget 9 | WHERE mb.user_id = save_user_budget.user_id AND mb.category_id = save_user_budget.category_id; 10 | ELSE 11 | INSERT INTO monthly_budgets (budget, user_id, category_id) 12 | VALUES (save_user_budget.budget, save_user_budget.user_id, save_user_budget.category_id); 13 | END IF; 14 | END; 15 | $$ LANGUAGE plpgsql; 16 | 17 | --select save_user_budget(3,2,700) 18 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/release/java/com/expensezen/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.expensezen; 8 | 9 | import android.content.Context; 10 | import com.facebook.react.ReactInstanceManager; 11 | 12 | /** 13 | * Class responsible of loading Flipper inside your React Native application. This is the release 14 | * flavor of it so it's empty as we don't want to load Flipper. 15 | */ 16 | public class ReactNativeFlipper { 17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 18 | // Do nothing as we don't want to initialize Flipper on Release. 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api/migrations/get_top_spendings.sql: -------------------------------------------------------------------------------- 1 | DROP function get_top_spendings 2 | 3 | create or replace function get_top_spendings(user_id integer, start_month date, end_month date) 4 | returns table (id integer,name text, total float, color text) AS $$ 5 | 6 | BEGIN 7 | return query 8 | SELECT c.id, c.name, SUM(e.amount) as total_spending, c.color from expenses e 9 | inner join users AS u on e.user_id = u.id 10 | inner join categories as c on c.id = e.category_id 11 | WHERE 12 | u.id = get_top_spendings.user_id and e.date >= get_top_spendings.start_month and e.date <= get_top_spendings.end_month 13 | GROUP BY 14 | c.name, c.id 15 | ORDER BY 16 | total_spending desc; 17 | END; 18 | 19 | $$ language plpgsql; 20 | 21 | --SELECT get_top_spendings(1, '2023-07-01', '2023-07-31') 22 | 23 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "ExpenseZen", 4 | "slug": "ExpenseZen", 5 | "version": "1.0.0", 6 | "scheme": "com.vladi.expensezen", 7 | "orientation": "portrait", 8 | "icon": "./assets/icon.png", 9 | "userInterfaceStyle": "light", 10 | "splash": { 11 | "image": "./assets/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "assetBundlePatterns": ["**/*"], 16 | "ios": { 17 | "supportsTablet": true, 18 | "bundleIdentifier": "com.vladi.expensezen" 19 | }, 20 | "android": { 21 | "package": "com.vladi.expensezen", 22 | "adaptiveIcon": { 23 | "foregroundImage": "./assets/adaptive-icon.png", 24 | "backgroundColor": "#ffffff" 25 | } 26 | }, 27 | "web": { 28 | "favicon": "./assets/favicon.png" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /components/AppContainer.tsx: -------------------------------------------------------------------------------- 1 | import { extendTheme, NativeBaseProvider } from "native-base"; 2 | import React from "react"; 3 | import { useSelector } from "react-redux"; 4 | import Navigation from "../navigation/Navigation"; 5 | import { RootState } from "../redux/store"; 6 | 7 | const AppContainer: React.FC = () => { 8 | const user = useSelector((state: RootState) => state.user); 9 | 10 | const isDarkTheme = user.theme === "dark"; 11 | 12 | const theme = extendTheme({ 13 | colors: { 14 | muted: { 15 | 50: isDarkTheme ? "#374151" : "#fafafa", 16 | 900: isDarkTheme ? "#fafafa" : "#171717", 17 | }, 18 | }, 19 | config: { 20 | initialColorMode: user.theme, 21 | }, 22 | }); 23 | 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default AppContainer; 32 | -------------------------------------------------------------------------------- /api/supabase.ts: -------------------------------------------------------------------------------- 1 | import "react-native-url-polyfill/auto"; 2 | import * as SecureStore from "expo-secure-store"; 3 | import { createClient } from "@supabase/supabase-js"; 4 | import { SUPABASE_URL, SUPABASE_ANON_KEY } from "@env"; 5 | import Constants from "expo-constants"; 6 | 7 | const ExposeSecureStoreAdapter = { 8 | getItem: (key: string) => { 9 | return SecureStore.getItemAsync(key); 10 | }, 11 | setItem: (key: string, value: string) => { 12 | SecureStore.setItemAsync(key, value); 13 | }, 14 | removeItem: (key: string) => { 15 | SecureStore.deleteItemAsync(key); 16 | }, 17 | }; 18 | 19 | export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { 20 | auth: { 21 | storage: ExposeSecureStoreAdapter as any, 22 | autoRefreshToken: true, 23 | persistSession: true, 24 | detectSessionInUrl: false, 25 | }, 26 | }); 27 | 28 | export const supabaseConfig = Constants.manifest?.extra?.supabase; 29 | -------------------------------------------------------------------------------- /components/shared/EZButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "native-base"; 2 | import { ResponsiveValue } from "native-base/lib/typescript/components/types"; 3 | import React from "react"; 4 | 5 | interface EZButtonProps { 6 | children: any; 7 | onPress: () => void; 8 | isLoading?: boolean; 9 | variant?: ResponsiveValue<"link" | "subtle" | "solid" | "ghost" | "outline" | "unstyled">; 10 | leftIcon?: JSX.Element; 11 | } 12 | 13 | interface EZButtonStypeProp { 14 | [key: string]: any; 15 | } 16 | 17 | const EZButton: React.FC = (props) => { 18 | const { children, onPress, isLoading, variant, leftIcon } = props; 19 | 20 | return ( 21 | 29 | ); 30 | }; 31 | 32 | export default EZButton; 33 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 17 | -------------------------------------------------------------------------------- /screens/ChangePasswordScreen.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "expo-status-bar"; 2 | import { VStack } from "native-base"; 3 | import React from "react"; 4 | import { SafeAreaView } from "react-native"; 5 | import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; 6 | import { useSelector } from "react-redux"; 7 | import ChangePasswordForm from "../components/ChangePasswordForm"; 8 | import { RootState } from "../redux/store"; 9 | 10 | const ChangePasswordScreen: React.FC = () => { 11 | const user = useSelector((state: RootState) => state.user); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default ChangePasswordScreen; 26 | -------------------------------------------------------------------------------- /redux/store.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 2 | import AsyncStorage from "@react-native-async-storage/async-storage"; 3 | import { persistReducer, persistStore } from "redux-persist"; 4 | import userReducer from "./userReducer"; 5 | import onboardReducer from "./onboardReducer"; 6 | import expensesReducers from "./expensesReducers"; 7 | 8 | const persistConfig = { 9 | key: "root", 10 | storage: AsyncStorage, 11 | blacklist: ["expenses"], 12 | }; 13 | 14 | const rootReducer = combineReducers({ 15 | user: userReducer, 16 | onboard: onboardReducer, 17 | expenses: expensesReducers, 18 | }); 19 | 20 | const persistedReducer = persistReducer(persistConfig, rootReducer); 21 | 22 | export const store = configureStore({ 23 | reducer: persistedReducer, 24 | middleware: (getDefaultMiddleware) => 25 | getDefaultMiddleware({ serializableCheck: false }), 26 | }); 27 | 28 | export const persistor = persistStore(store); 29 | 30 | export type RootState = ReturnType; 31 | -------------------------------------------------------------------------------- /screens/ResetPasswordScreen.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "expo-status-bar"; 2 | import { VStack } from "native-base"; 3 | import React from "react"; 4 | import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; 5 | import { SafeAreaView } from "react-native-safe-area-context"; 6 | import { useSelector } from "react-redux"; 7 | import ResetPasswordForm from "../components/ResetPasswordForm"; 8 | import { RootState } from "../redux/store"; 9 | 10 | const ResetPasswordScreen: React.FC = () => { 11 | const user = useSelector((state: RootState) => state.user); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default ResetPasswordScreen; 26 | -------------------------------------------------------------------------------- /navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavigationContainer, DefaultTheme, DarkTheme } from "@react-navigation/native"; 3 | 4 | import StackNavigator from "./StackNavigator"; 5 | import { useSelector } from "react-redux"; 6 | import { RootState } from "../redux/store"; 7 | 8 | const Navigation: React.FC = () => { 9 | const { theme } = useSelector((state: RootState) => state.user); 10 | 11 | let navigationTheme = theme === "light" ? DefaultTheme : DarkTheme; 12 | 13 | navigationTheme = { 14 | ...navigationTheme, 15 | colors: { 16 | ...navigationTheme.colors, 17 | background: theme === "dark" ? "#1f2937" : navigationTheme.colors.background, 18 | card: theme === "dark" ? "#374151" : navigationTheme.colors.card, 19 | primary: "#222", 20 | 21 | //text: "red", 22 | }, 23 | }; 24 | 25 | return ( 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default Navigation; 33 | -------------------------------------------------------------------------------- /components/OnboardingStep.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useWindowDimensions } from "react-native"; 3 | import { Text, VStack } from "native-base"; 4 | import { Step } from "../interfaces/Step"; 5 | 6 | interface OnboardingStepProps { 7 | step: Step; 8 | } 9 | 10 | const OnboardingStep: React.FC = ({ step }) => { 11 | const { width } = useWindowDimensions(); 12 | 13 | const { title, description, image } = step; 14 | return ( 15 | 16 | 17 | {image} 18 | 19 | 20 | {title} 21 | 22 | 23 | {description} 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default OnboardingStep; 32 | -------------------------------------------------------------------------------- /colors.ts: -------------------------------------------------------------------------------- 1 | const COLORS = { 2 | PURPLE: { 3 | 50: "#faf5ff", 4 | 100: "#f3e8ff", 5 | 200: "#e9d5ff", 6 | 300: "#d8b4fe", 7 | 400: "#c084fc", 8 | 500: "#a855f7", 9 | 600: "#9333ea", 10 | 700: "#7e22ce", 11 | 800: "#6b21a8", 12 | 900: "#581c87", 13 | }, 14 | PINK: { 15 | 300: "#f0abfc", 16 | 400: "#e879f9", 17 | 500: "#d946ef", 18 | 600: "#c026d3", 19 | 700: "#a21caf", 20 | 800: "#86198f", 21 | 900: "#701a75", 22 | }, 23 | BLUE: { 24 | 400: "#60a5fa", 25 | 500: "#3b82f6", 26 | 600: "#2563eb", 27 | }, 28 | EMERALD: { 29 | 300: "#6ee7b7", 30 | 400: "#34d399", 31 | 500: "#10b981", 32 | }, 33 | DANGER: { 34 | 400: "#f87171", 35 | 500: "#ef4444", 36 | 600: "#dc2626", 37 | }, 38 | YELLOW: { 39 | 400: "#facc15", 40 | 500: "#eab308", 41 | }, 42 | MUTED: { 43 | 50: "#fafafa", 44 | 100: "#f5f5f5", 45 | 200: "#e5e5e5", 46 | 300: "#d4d4d4", 47 | 400: "#a3a3a3", 48 | 500: "#737373", 49 | 600: "#525252", 50 | 700: "#404040", 51 | 800: "#262626", 52 | 900: "#171717", 53 | }, 54 | }; 55 | 56 | export default COLORS; 57 | -------------------------------------------------------------------------------- /api/services/CategoryService.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { supabase } from "../supabase"; 3 | import { CATEGORIES } from "../../constants/Tables"; 4 | import { GET_TOP_SPENDINGS } from "../../constants/PostgresFunctions"; 5 | 6 | const getAllCategories = async () => { 7 | try { 8 | const { data } = await supabase.from(CATEGORIES).select("*"); 9 | 10 | return data; 11 | } catch (error) { 12 | if (error instanceof Error) { 13 | console.log(error); 14 | } 15 | } 16 | }; 17 | 18 | const getTopSpendingCategories = async (userId: number) => { 19 | const startOfMonth = moment().startOf("month").format("YYYY-MM-DD"); 20 | const endOfMonth = moment().endOf("month").format("YYYY-MM-DD"); 21 | 22 | try { 23 | const { data } = await supabase.rpc(GET_TOP_SPENDINGS, { 24 | user_id: userId, 25 | start_month: startOfMonth, 26 | end_month: endOfMonth, 27 | }); 28 | 29 | return data; 30 | } catch (error) { 31 | if (error instanceof Error) { 32 | console.log(error); 33 | } 34 | } 35 | }; 36 | 37 | export const CategoryService = { 38 | getAllCategories, 39 | getTopSpendingCategories, 40 | }; 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.env 7 | *.p8 8 | *.p12 9 | *.key 10 | *.mobileprovision 11 | *.orig.* 12 | web-build/ 13 | 14 | # macOS 15 | .DS_Store 16 | 17 | # Temporary files created by Metro to check the health of the file watcher 18 | .metro-health-check* 19 | 20 | # @generated expo-cli sync-647791c5bd841d5c91864afb91c302553d300921 21 | # The following patterns were generated by expo-cli 22 | 23 | # OSX 24 | # 25 | .DS_Store 26 | 27 | # Xcode 28 | # 29 | build/ 30 | *.pbxuser 31 | !default.pbxuser 32 | *.mode1v3 33 | !default.mode1v3 34 | *.mode2v3 35 | !default.mode2v3 36 | *.perspectivev3 37 | !default.perspectivev3 38 | xcuserdata 39 | *.xccheckout 40 | *.moved-aside 41 | DerivedData 42 | *.hmap 43 | *.ipa 44 | *.xcuserstate 45 | project.xcworkspace 46 | 47 | # Android/IntelliJ 48 | # 49 | build/ 50 | .idea 51 | .gradle 52 | local.properties 53 | *.iml 54 | *.hprof 55 | .cxx/ 56 | *.keystore 57 | !debug.keystore 58 | 59 | # node.js 60 | # 61 | node_modules/ 62 | npm-debug.log 63 | yarn-error.log 64 | 65 | # Bundle artifacts 66 | *.jsbundle 67 | 68 | # CocoaPods 69 | /ios/Pods/ 70 | 71 | # Temporary files created by Metro to check the health of the file watcher 72 | .metro-health-check* 73 | 74 | # Expo 75 | .expo/ 76 | web-build/ 77 | dist/ 78 | 79 | # @end expo-cli -------------------------------------------------------------------------------- /components/Paginator.tsx: -------------------------------------------------------------------------------- 1 | import { useWindowDimensions, Animated } from "react-native"; 2 | import React from "react"; 3 | import { HStack, View } from "native-base"; 4 | 5 | const Paginator: React.FC = ({ steps, scrollX }) => { 6 | const { width: windowWidth } = useWindowDimensions(); 7 | 8 | const AnimatedView = Animated.createAnimatedComponent(View); 9 | 10 | return ( 11 | 12 | {steps.map((_: any, index: number) => { 13 | const inputRange = [ 14 | windowWidth * (index - 1), 15 | windowWidth * index, 16 | windowWidth * (index + 1), 17 | ]; 18 | 19 | const width = scrollX.interpolate({ 20 | inputRange, 21 | outputRange: [9, 18, 9], 22 | extrapolate: "clamp", 23 | }); 24 | 25 | const opacity = scrollX.interpolate({ 26 | inputRange, 27 | outputRange: [0.4, 1, 0.4], 28 | extrapolate: "clamp", 29 | }); 30 | 31 | return ( 32 | 40 | ); 41 | })} 42 | 43 | ); 44 | }; 45 | 46 | export default Paginator; 47 | -------------------------------------------------------------------------------- /assets/SVG/Google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/getCategoryIcon.js: -------------------------------------------------------------------------------- 1 | import { 2 | Ionicons, 3 | MaterialIcons, 4 | FontAwesome, 5 | MaterialCommunityIcons, 6 | FontAwesome5, 7 | } from "@expo/vector-icons"; 8 | import COLORS from "../colors"; 9 | 10 | export const getCategoryIcon = (name, size, color) => { 11 | let icon; 12 | 13 | switch (name) { 14 | case "Grocery": 15 | icon = ; 16 | break; 17 | case "Fuel": 18 | icon = ( 19 | 20 | ); 21 | break; 22 | case "Food & Drink": 23 | icon = ; 24 | break; 25 | case "Clothes": 26 | icon = ( 27 | 28 | ); 29 | break; 30 | case "Gifts": 31 | icon = ; 32 | break; 33 | case "Travel": 34 | icon = ( 35 | 36 | ); 37 | break; 38 | case "Medicine": 39 | icon = ; 40 | break; 41 | case "Bills": 42 | icon = ; 43 | break; 44 | default: 45 | icon = null; 46 | } 47 | 48 | return icon; 49 | }; 50 | -------------------------------------------------------------------------------- /api/services/CurrencyService.ts: -------------------------------------------------------------------------------- 1 | import { FREECURRENCY_API_KEY } from "@env"; 2 | import { USERS } from "../../constants/Tables"; 3 | import { supabase } from "../supabase"; 4 | import axios from "axios"; 5 | 6 | const apiUrl = "https://api.freecurrencyapi.com/v1"; 7 | 8 | const updateUserCurrency = async (userId: number, currencySymbol: string, currencyCode: string) => { 9 | try { 10 | const { error } = await supabase 11 | .from(USERS) 12 | .update({ currency_code: currencyCode, currency_symbol: currencySymbol }) 13 | .filter("id", "eq", userId); 14 | 15 | if (error) { 16 | throw error; 17 | } 18 | } catch (error) { 19 | if (error instanceof Error) { 20 | console.log(error); 21 | } 22 | } 23 | }; 24 | 25 | const getAllCurrencies = async () => { 26 | const { data } = await axios.get(`${apiUrl}/currencies`, { 27 | params: { apikey: FREECURRENCY_API_KEY }, 28 | }); 29 | return data.data; 30 | }; 31 | 32 | const getCurrencyConversionRate = async (baseCurrency: string, currencyToChange: string) => { 33 | try { 34 | const { data } = await axios.get(`${apiUrl}/latest`, { 35 | params: { 36 | apikey: FREECURRENCY_API_KEY, 37 | currencies: currencyToChange, 38 | base_currency: baseCurrency, 39 | }, 40 | }); 41 | return data.data; 42 | } catch (error) { 43 | console.log(error); 44 | } 45 | }; 46 | 47 | export const CurrencyService = { 48 | updateUserCurrency, 49 | getAllCurrencies, 50 | getCurrencyConversionRate, 51 | }; 52 | -------------------------------------------------------------------------------- /components/MonthlyBudgetItem.tsx: -------------------------------------------------------------------------------- 1 | import { Box, HStack, Text } from "native-base"; 2 | import React, { useState } from "react"; 3 | 4 | import EZInput from "./shared/EZInput"; 5 | import { Category } from "../interfaces/Category"; 6 | import { authInput } from "../commonStyles"; 7 | 8 | interface MonthlyBudgetItemProps { 9 | category: Category; 10 | onChange: (e: string) => void; 11 | } 12 | 13 | const MonthlyBudgetItem: React.FC = ({ category, onChange }) => { 14 | const { color, icon, name, budget } = category; 15 | 16 | const [amount, setAmount] = useState(budget!.toString().replace(".", ",")); 17 | 18 | const handleChange = (e: string) => { 19 | setAmount(e); 20 | onChange(e); 21 | }; 22 | 23 | return ( 24 | 25 | 26 | 33 | {icon} 34 | 35 | {name} 36 | 37 | handleChange(e)} 46 | borderRadius={12} 47 | borderColor="muted.200" 48 | alignItems="flex-end" 49 | /> 50 | 51 | ); 52 | }; 53 | 54 | export default MonthlyBudgetItem; 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExpenseZen 2 | Mobile app built with React Native, TypeScript, Native Base, Supabase and Redux 3 | 4 | ## About 5 | This mobile application is designed to help users take control of their finances effortlessly. 6 | With the help of ExpenseZen, users can easily create an account, manage their spending based on various categories, 7 | visualize their expenses through intuitive pie charts, set monthly budgets, and gain valuable insights into their financial habits month by month. 8 | 9 | ## Key Features 10 | * Sign Up / Sign In 11 | * Add Expense(s) based on certain category/categories 12 | * Pie Chart: Visualize your spending patterns with a pie chart 13 | * Monthly Budgets: Set monthly budgets to help you stay on track and avoid overspending in specific categories. 14 | * Keep track on all expenses from a specific category in the current month 15 | * Dark theme support 16 | 17 | ## Screenshots 18 | 19 | ### Onboarding 20 | 21 | 22 | ### Sign Up / Sign In 23 | 24 | 25 | ### Main Screens 26 | 27 | 28 | ### Add Expense/Budget & Vizualize Expenses 29 | 30 | 31 | ### Dark theme 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /screens/CategoriesScreen.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 2 | import { StatusBar } from "expo-status-bar"; 3 | import { View } from "native-base"; 4 | import React, { useLayoutEffect } from "react"; 5 | import { FlatList } from "react-native"; 6 | import CategoryItem from "../components/CategoryItem"; 7 | import { useWindowDimensions } from "react-native"; 8 | import EZHeaderTitle from "../components/shared/EzHeaderTitle"; 9 | import { useSelector } from "react-redux"; 10 | import { categoriesSelector } from "../redux/expensesReducers"; 11 | 12 | interface CategoriesScrennProps { 13 | navigation: NavigationProp; 14 | } 15 | 16 | const CategoriesScreen: React.FC = ({ navigation }) => { 17 | const { width } = useWindowDimensions(); 18 | const categories = useSelector(categoriesSelector); 19 | 20 | useLayoutEffect(() => { 21 | navigation.setOptions({ 22 | headerTitle: () => Categories, 23 | }); 24 | }, [navigation]); 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | } 40 | data={categories} 41 | renderItem={({ item }) => } 42 | /> 43 | 44 | 45 | ); 46 | }; 47 | export default CategoriesScreen; 48 | -------------------------------------------------------------------------------- /components/TopSpendingCategory.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text, useTheme, VStack } from "native-base"; 2 | import React from "react"; 3 | import { Category } from "../interfaces/Category"; 4 | import { getCategoryIcon } from "../utils/getCategoryIcon"; 5 | import { useNavigation } from "@react-navigation/native"; 6 | import { Expense } from "../interfaces/Expense"; 7 | import { TouchableOpacity } from "react-native"; 8 | import { NativeStackNavigationProp } from "@react-navigation/native-stack"; 9 | import { AppStackParamList } from "../interfaces/Navigation"; 10 | 11 | interface TopSpendingCategoryProps { 12 | item: Category; 13 | expenses: Expense[]; 14 | } 15 | 16 | const TopSpendingCategory: React.FC = ({ item, expenses }) => { 17 | const { name, color } = item; 18 | const { 19 | colors: { muted }, 20 | } = useTheme(); 21 | 22 | const navigation = useNavigation>(); 23 | 24 | const goToCategoryExpenses = () => { 25 | navigation.navigate("CategoryExpenses", { 26 | expenses, 27 | name, 28 | }); 29 | }; 30 | 31 | return ( 32 | 33 | 34 | 42 | {getCategoryIcon(name, 32, muted[50])} 43 | 44 | {name} 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default TopSpendingCategory; 51 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { useFonts } from "expo-font"; 2 | import { LogBox } from "react-native"; 3 | import SourceSansPro from "./assets/fonts/SourceSansProRegular.ttf"; 4 | import SourceBold from "./assets/fonts/SourceSansProBold.ttf"; 5 | import { PersistGate } from "redux-persist/integration/react"; 6 | import { Provider } from "react-redux"; 7 | import { persistor, store } from "./redux/store"; 8 | import AppContainer from "./components/AppContainer"; 9 | 10 | LogBox.ignoreLogs([ 11 | "Using Math.random is not cryptographically secure! Use bcrypt.setRandomFallback to set a PRNG.", 12 | "VirtualizedLists should never be nested inside plain ScrollViews with the same orientation because it can break windowing and other functionality - use another VirtualizedList-backed container instead.", 13 | "In React 18, SSRProvider is not necessary and is a noop. You can remove it from your app.", 14 | "@supabase/gotrue-js: Stack guards not supported in this environment. Generally not an issue but may point to a very conservative transpilation environment (use ES2017 or above) that implements async/await with generators, or this is a JavaScript engine that does not support async/await stack traces. Safari is known to not support stack guards.", 15 | "Constants.platform.ios.model has been deprecated in favor of expo-device's Device.modelName property. This API will be removed in SDK 45.", 16 | ]); 17 | 18 | export default function App() { 19 | const [fontsLoaded] = useFonts({ 20 | SourceSansPro, 21 | SourceBold, 22 | }); 23 | 24 | if (!fontsLoaded) { 25 | return null; 26 | } 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = findProperty('android.buildToolsVersion') ?: '33.0.0' 6 | minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21') 7 | compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '33') 8 | targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '33') 9 | if (findProperty('android.kotlinVersion')) { 10 | kotlinVersion = findProperty('android.kotlinVersion') 11 | } 12 | frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0' 13 | 14 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. 15 | ndkVersion = "23.1.7779620" 16 | } 17 | repositories { 18 | google() 19 | mavenCentral() 20 | } 21 | dependencies { 22 | classpath('com.android.tools.build:gradle:7.4.1') 23 | classpath('com.facebook.react:react-native-gradle-plugin') 24 | } 25 | } 26 | 27 | allprojects { 28 | repositories { 29 | maven { 30 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 31 | url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) 32 | } 33 | maven { 34 | // Android JSC is installed from npm 35 | url(new File(['node', '--print', "require.resolve('jsc-android/package.json')"].execute(null, rootDir).text.trim(), '../dist')) 36 | } 37 | 38 | google() 39 | mavenCentral() 40 | maven { url 'https://www.jitpack.io' } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /screens/RegisterScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SafeAreaView, TouchableOpacity } from "react-native"; 3 | import { HStack, Text, VStack } from "native-base"; 4 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 5 | import RegisterForm from "../components/RegisterForm"; 6 | import { StatusBar } from "expo-status-bar"; 7 | import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; 8 | import { useSelector } from "react-redux"; 9 | import { RootState } from "../redux/store"; 10 | 11 | interface RegisterScreenProps { 12 | navigation: NavigationProp; 13 | } 14 | 15 | const RegisterScreen: React.FC = ({ navigation }) => { 16 | const user = useSelector((state: RootState) => state.user); 17 | 18 | return ( 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Already have an account ? 31 | 32 | 33 | 38 | Sign in 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default RegisterScreen; 49 | -------------------------------------------------------------------------------- /components/shared/EZProgress.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { Animated } from "react-native"; 3 | import { View } from "native-base"; 4 | import { useSelector } from "react-redux"; 5 | import { RootState } from "../../redux/store"; 6 | 7 | interface EZProgresssProps { 8 | height: string | number; 9 | value: number; 10 | maxValue: number; 11 | color: string | undefined; 12 | } 13 | 14 | const EZProgress: React.FC = ({ height, value, maxValue, color }) => { 15 | const animatedValue = useRef(new Animated.Value(-1000)).current; 16 | const reactive = useRef(new Animated.Value(-1000)).current; 17 | const [width, setWidth] = useState(0); 18 | const user = useSelector((state: RootState) => state.user); 19 | 20 | useEffect(() => { 21 | Animated.timing(animatedValue, { 22 | toValue: reactive, 23 | duration: 700, 24 | useNativeDriver: true, 25 | }).start(); 26 | }, []); 27 | 28 | useEffect(() => { 29 | let progressValue: number; 30 | if (value >= maxValue) { 31 | progressValue = 1; 32 | } else { 33 | progressValue = -width + (width * value) / maxValue; 34 | } 35 | reactive.setValue(progressValue); 36 | }, [value, width, maxValue]); 37 | 38 | const AnimatedView = Animated.createAnimatedComponent(View); 39 | 40 | return ( 41 | { 43 | const width = e.nativeEvent.layout.width; 44 | setWidth(width); 45 | }} 46 | bg={user.theme === "dark" ? "muted.500" : "muted.200"} 47 | borderRadius={height} 48 | overflow="hidden" 49 | height={height}> 50 | 62 | 63 | ); 64 | }; 65 | 66 | export default EZProgress; 67 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /redux/userReducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import moment from "moment"; 3 | 4 | const currentMonth = moment().format("MMMM"); 5 | 6 | interface initalStateProps { 7 | id: number; 8 | firstName: string | null; 9 | lastName: string | null; 10 | email: string | null; 11 | currency: string | null; 12 | symbol: string | null; 13 | theme: "light" | "dark"; 14 | month: string; 15 | } 16 | 17 | const initialState: initalStateProps = { 18 | id: 0, 19 | firstName: null, 20 | lastName: null, 21 | email: null, 22 | currency: null, 23 | symbol: null, 24 | theme: "light", 25 | month: currentMonth, 26 | }; 27 | 28 | const userReducer = createSlice({ 29 | name: "user", 30 | 31 | initialState: initialState, 32 | reducers: { 33 | setUser: (state, action) => { 34 | const { payload } = action; 35 | state.id = payload.id; 36 | state.firstName = payload.firstName; 37 | state.lastName = payload.lastName; 38 | state.email = payload.email; 39 | }, 40 | removeUser: (state) => { 41 | state.id = 0; 42 | state.firstName = null; 43 | state.lastName = null; 44 | state.email = null; 45 | }, 46 | setCurrency: (state, action) => { 47 | const { payload } = action; 48 | state.currency = payload.name; 49 | state.symbol = payload.symbol; 50 | }, 51 | removeCurrency: (state) => { 52 | state.currency = null; 53 | state.symbol = null; 54 | }, 55 | setTheme: (state, action) => { 56 | state.theme = action.payload; 57 | }, 58 | setMonth: (state, action) => { 59 | state.month = action.payload; 60 | }, 61 | }, 62 | }); 63 | 64 | export const setUser = userReducer.actions.setUser; 65 | export const removeUser = userReducer.actions.removeUser; 66 | export const setCurrency = userReducer.actions.setCurrency; 67 | export const removeCurrency = userReducer.actions.removeCurrency; 68 | export const setThemeAction = userReducer.actions.setTheme; 69 | export const setMonthAction = userReducer.actions.setMonth; 70 | 71 | export default userReducer.reducer; 72 | -------------------------------------------------------------------------------- /screens/LoginScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SafeAreaView, TouchableOpacity } from "react-native"; 3 | import { Text, VStack, HStack } from "native-base"; 4 | import LoginForm from "../components/LoginForm"; 5 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 6 | import { StatusBar } from "expo-status-bar"; 7 | import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; 8 | import { useSelector } from "react-redux"; 9 | import { RootState } from "../redux/store"; 10 | 11 | interface LoginScreenProps { 12 | navigation: NavigationProp; 13 | } 14 | 15 | const LoginScreen: React.FC = ({ navigation }) => { 16 | const goToRegister = () => { 17 | navigation.navigate("Register"); 18 | }; 19 | const user = useSelector((state: RootState) => state.user); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | {/* 29 | 30 | Or continue with 31 | 32 | 33 | */} 34 | 35 | 36 | Don't have an account ? 37 | 38 | 39 | 44 | Sign up 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default LoginScreen; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expensezen", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "expo start --dev-client", 6 | "android": "expo run:android", 7 | "ios": "expo run:ios", 8 | "web": "expo start --web" 9 | }, 10 | "dependencies": { 11 | "@react-aria/ssr": "^3.7.0", 12 | "@react-native-async-storage/async-storage": "1.17.11", 13 | "@react-native-picker/picker": "2.4.8", 14 | "@react-navigation/bottom-tabs": "^6.5.8", 15 | "@react-navigation/native": "^6.1.6", 16 | "@react-navigation/native-stack": "^6.9.12", 17 | "@reduxjs/toolkit": "^1.9.5", 18 | "@supabase/supabase-js": "^2.25.0", 19 | "@types/bcrypt": "^5.0.0", 20 | "@types/react-dom": "18.0.10", 21 | "@types/react-native-charts-wrapper": "^0.5.5", 22 | "add": "^2.0.6", 23 | "axios": "^1.4.0", 24 | "expo": "~48.0.18", 25 | "expo-auth-session": "4.0.3", 26 | "expo-constants": "~14.2.1", 27 | "expo-font": "~11.1.1", 28 | "expo-linear-gradient": "~12.1.2", 29 | "expo-secure-store": "12.1.1", 30 | "expo-splash-screen": "~0.18.2", 31 | "expo-status-bar": "~1.4.4", 32 | "formik": "^2.4.2", 33 | "install": "^0.13.0", 34 | "isaac": "^0.0.5", 35 | "moment": "^2.29.4", 36 | "native-base": "^3.4.28", 37 | "npm": "^9.7.2", 38 | "react": "18.2.0", 39 | "react-native": "0.71.8", 40 | "react-native-bcrypt": "^2.4.0", 41 | "react-native-dotenv": "^3.4.9", 42 | "react-native-gesture-handler": "2.9.0", 43 | "react-native-keyboard-aware-scroll-view": "^0.9.5", 44 | "react-native-picker-select": "^8.0.4", 45 | "react-native-pie-chart": "^3.0.1", 46 | "react-native-reanimated": "~2.14.4", 47 | "react-native-safe-area-context": "4.5.0", 48 | "react-native-screens": "~3.20.0", 49 | "react-native-svg": "13.4.0", 50 | "react-native-svg-transformer": "^1.0.0", 51 | "react-native-url-polyfill": "^1.3.0", 52 | "react-redux": "^8.1.1", 53 | "redux-persist": "^6.0.0", 54 | "yarn": "^1.22.19", 55 | "yup": "^1.2.0" 56 | }, 57 | "devDependencies": { 58 | "@babel/core": "^7.20.0", 59 | "@types/isaac": "^0.0.0", 60 | "@types/react": "^18.2.14", 61 | "@types/react-native": "^0.72.2", 62 | "@types/react-native-bcrypt": "^2.4.3", 63 | "typescript": "^4.9.4" 64 | }, 65 | "private": true 66 | } 67 | -------------------------------------------------------------------------------- /api/services/ExpenseService.ts: -------------------------------------------------------------------------------- 1 | import { CONVERT_EXPENSES_CURRENCY, GET_MONTH_EXPENSES } from "../../constants/PostgresFunctions"; 2 | import { EXPENSES } from "../../constants/Tables"; 3 | import { Expense } from "../../interfaces/Expense"; 4 | import { getCurrentDate } from "../../utils/getCurrentDate"; 5 | import { supabase } from "../supabase"; 6 | 7 | // const startMonth = moment().startOf("month").format("YYYY-MM-DD"); 8 | // const endMonth = moment().endOf("month").format("YYYY-MM-DD"); 9 | 10 | const AddExpense = async (expense: Expense) => { 11 | const { amount, categoryId, description, userId } = expense; 12 | 13 | const currentDate = getCurrentDate(); 14 | 15 | await supabase.from("expenses").insert({ 16 | user_id: userId, 17 | category_id: categoryId, 18 | amount, 19 | description, 20 | date: currentDate, 21 | }); 22 | 23 | try { 24 | } catch (error) { 25 | if (error instanceof Error) { 26 | console.log(error); 27 | } 28 | } 29 | }; 30 | 31 | const getMonthExpenses = async (userId: number, startOfMonth: string, endOfMonth: string) => { 32 | try { 33 | const { data } = await supabase.rpc(GET_MONTH_EXPENSES, { 34 | start_month: startOfMonth, 35 | end_month: endOfMonth, 36 | user_id: userId, 37 | }); 38 | 39 | return data; 40 | } catch (error) { 41 | if (error instanceof Error) { 42 | console.log(error); 43 | } 44 | } 45 | }; 46 | 47 | const convertExpensesCurrency = async (userId: number, conversionRate: number) => { 48 | try { 49 | const { error } = await supabase.rpc(CONVERT_EXPENSES_CURRENCY, { 50 | user_id: userId, 51 | conversion_rate: conversionRate, 52 | }); 53 | 54 | if (error) { 55 | throw error; 56 | } 57 | } catch (error) { 58 | if (error instanceof Error) { 59 | console.log(error); 60 | } 61 | } 62 | }; 63 | 64 | const removeUserExpenses = async (userId: number) => { 65 | try { 66 | const { error } = await supabase.from(EXPENSES).delete().filter("user_id", "eq", userId); 67 | 68 | if (error) { 69 | throw error; 70 | } 71 | } catch (error) { 72 | if (error instanceof Error) { 73 | console.log(error); 74 | } 75 | } 76 | }; 77 | 78 | export const ExpenseService = { 79 | AddExpense, 80 | getMonthExpenses, 81 | convertExpensesCurrency, 82 | removeUserExpenses, 83 | }; 84 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Automatically convert third-party libraries to use AndroidX 26 | android.enableJetifier=true 27 | 28 | # Version of flipper SDK to use with React Native 29 | FLIPPER_VERSION=0.125.0 30 | 31 | # Use this property to specify which architecture you want to build. 32 | # You can also override it from the CLI using 33 | # ./gradlew -PreactNativeArchitectures=x86_64 34 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 35 | 36 | # Use this property to enable support to the new architecture. 37 | # This will allow you to use TurboModules and the Fabric render in 38 | # your application. You should enable this flag either if you want 39 | # to write custom TurboModules/Fabric components OR use libraries that 40 | # are providing them. 41 | newArchEnabled=false 42 | 43 | # The hosted JavaScript engine 44 | # Supported values: expo.jsEngine = "hermes" | "jsc" 45 | expo.jsEngine=jsc 46 | 47 | # Enable GIF support in React Native images (~200 B increase) 48 | expo.gif.enabled=true 49 | # Enable webp support in React Native images (~85 KB increase) 50 | expo.webp.enabled=true 51 | # Enable animated webp support (~3.4 MB increase) 52 | # Disabled by default because iOS doesn't support animated webp 53 | expo.webp.animated=false 54 | -------------------------------------------------------------------------------- /components/CategoryItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, HStack, Pressable, Text } from "native-base"; 3 | import { Category } from "../interfaces/Category"; 4 | import { getCategoryIcon } from "../utils/getCategoryIcon"; 5 | import { AntDesign } from "@expo/vector-icons"; 6 | import COLORS from "../colors"; 7 | import { useSelector } from "react-redux"; 8 | import { RootState } from "../redux/store"; 9 | 10 | interface CategoryItemProps { 11 | category: Category; 12 | selectCategory?: (e: string) => void; 13 | selectedCategory?: string; 14 | disabled: boolean; 15 | } 16 | 17 | const CategoryItem: React.FC = ({ 18 | category, 19 | selectCategory, 20 | selectedCategory, 21 | disabled, 22 | }) => { 23 | const user: any = useSelector((state: RootState) => state.user); 24 | 25 | const { name, color } = category; 26 | 27 | const isSelected = selectedCategory === name; 28 | 29 | return ( 30 | selectCategory!(name)} 33 | _pressed={{ opacity: 0.4 }} 34 | marginX={3} 35 | flex={1} 36 | width="175px" 37 | onStartShouldSetResponder={() => true} 38 | borderColor={isSelected ? COLORS.EMERALD[400] : "muted.100"} 39 | borderWidth={user.theme === "dark" ? 0 : 1.5} 40 | height={65} 41 | bg="muted.50" 42 | style={{ 43 | shadowColor: "#171717", 44 | shadowOffset: { width: 0, height: 0 }, 45 | shadowOpacity: 0.1, 46 | shadowRadius: 4, 47 | }} 48 | borderRadius={20} 49 | px={2}> 50 | 51 | 58 | {getCategoryIcon(name, 24, COLORS.MUTED[50])} 59 | 60 | 61 | {name} 62 | 63 | 64 | {isSelected && ( 65 | 66 | 67 | 68 | )} 69 | 70 | ); 71 | }; 72 | 73 | export default CategoryItem; 74 | -------------------------------------------------------------------------------- /components/GraphCategoryItem.tsx: -------------------------------------------------------------------------------- 1 | import { Box, HStack, Text, VStack } from "native-base"; 2 | import React from "react"; 3 | import { TouchableOpacity } from "react-native"; 4 | import { GraphCategory } from "../interfaces/GraphCategory"; 5 | import { getCategoryIcon } from "../utils/getCategoryIcon"; 6 | import { useSelector } from "react-redux"; 7 | import { RootState } from "../redux/store"; 8 | import COLORS from "../colors"; 9 | import { useNavigation } from "@react-navigation/native"; 10 | import { NativeStackNavigationProp } from "@react-navigation/native-stack"; 11 | import { AppStackParamList } from "../interfaces/Navigation"; 12 | import { Expense } from "../interfaces/Expense"; 13 | 14 | interface GraphCategoryItemProps { 15 | graphCategory: GraphCategory; 16 | expenses: any[]; 17 | } 18 | 19 | const GraphCategoryItem: React.FC = ({ graphCategory, expenses }) => { 20 | const { color, name } = graphCategory; 21 | 22 | const user = useSelector((state: RootState) => state.user); 23 | const navigation = useNavigation>(); 24 | 25 | const goToCategoryExpenses = () => { 26 | navigation.navigate("CategoryExpenses", { 27 | expenses, 28 | name, 29 | }); 30 | }; 31 | 32 | const amount = expenses.reduce( 33 | (accumulator: any, currentValue: Expense) => accumulator + currentValue.amount, 34 | 0 35 | ); 36 | 37 | return ( 38 | 39 | 40 | 41 | 48 | {getCategoryIcon(name, 22, COLORS.MUTED[50])} 49 | 50 | 51 | 52 | {name} 53 | 54 | 55 | {expenses.length} expenses 56 | 57 | 58 | 59 | 60 | {user.symbol} {amount.toFixed(2)} 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | export default GraphCategoryItem; 68 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /assets/SVG/NoData.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/vladi/ExpenseZen/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.vladi.ExpenseZen; 2 | 3 | import android.app.Application; 4 | import android.content.res.Configuration; 5 | import androidx.annotation.NonNull; 6 | 7 | import com.facebook.react.PackageList; 8 | import com.facebook.react.ReactApplication; 9 | import com.facebook.react.ReactNativeHost; 10 | import com.facebook.react.ReactPackage; 11 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 12 | import com.facebook.react.defaults.DefaultReactNativeHost; 13 | import com.facebook.soloader.SoLoader; 14 | 15 | import expo.modules.ApplicationLifecycleDispatcher; 16 | import expo.modules.ReactNativeHostWrapper; 17 | 18 | import java.util.List; 19 | 20 | public class MainApplication extends Application implements ReactApplication { 21 | 22 | private final ReactNativeHost mReactNativeHost = 23 | new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) { 24 | @Override 25 | public boolean getUseDeveloperSupport() { 26 | return BuildConfig.DEBUG; 27 | } 28 | 29 | @Override 30 | protected List getPackages() { 31 | @SuppressWarnings("UnnecessaryLocalVariable") 32 | List packages = new PackageList(this).getPackages(); 33 | // Packages that cannot be autolinked yet can be added manually here, for example: 34 | // packages.add(new MyReactNativePackage()); 35 | return packages; 36 | } 37 | 38 | @Override 39 | protected String getJSMainModuleName() { 40 | return "index"; 41 | } 42 | 43 | @Override 44 | protected boolean isNewArchEnabled() { 45 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 46 | } 47 | 48 | @Override 49 | protected Boolean isHermesEnabled() { 50 | return BuildConfig.IS_HERMES_ENABLED; 51 | } 52 | }); 53 | 54 | @Override 55 | public ReactNativeHost getReactNativeHost() { 56 | return mReactNativeHost; 57 | } 58 | 59 | @Override 60 | public void onCreate() { 61 | super.onCreate(); 62 | SoLoader.init(this, /* native exopackage */ false); 63 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 64 | // If you opted-in for the New Architecture, we load the native entry point for this app. 65 | DefaultNewArchitectureEntryPoint.load(); 66 | } 67 | ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 68 | ApplicationLifecycleDispatcher.onApplicationCreate(this); 69 | } 70 | 71 | @Override 72 | public void onConfigurationChanged(@NonNull Configuration newConfig) { 73 | super.onConfigurationChanged(newConfig); 74 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/vladi/ExpenseZen/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.vladi.ExpenseZen; 2 | 3 | import android.os.Build; 4 | import android.os.Bundle; 5 | 6 | import com.facebook.react.ReactActivity; 7 | import com.facebook.react.ReactActivityDelegate; 8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 9 | import com.facebook.react.defaults.DefaultReactActivityDelegate; 10 | 11 | import expo.modules.ReactActivityDelegateWrapper; 12 | 13 | public class MainActivity extends ReactActivity { 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | // Set the theme to AppTheme BEFORE onCreate to support 17 | // coloring the background, status bar, and navigation bar. 18 | // This is required for expo-splash-screen. 19 | setTheme(R.style.AppTheme); 20 | super.onCreate(null); 21 | } 22 | 23 | /** 24 | * Returns the name of the main component registered from JavaScript. 25 | * This is used to schedule rendering of the component. 26 | */ 27 | @Override 28 | protected String getMainComponentName() { 29 | return "main"; 30 | } 31 | 32 | /** 33 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link 34 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React 35 | * (aka React 18) with two boolean flags. 36 | */ 37 | @Override 38 | protected ReactActivityDelegate createReactActivityDelegate() { 39 | return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate( 40 | this, 41 | getMainComponentName(), 42 | // If you opted-in for the New Architecture, we enable the Fabric Renderer. 43 | DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled 44 | // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18). 45 | DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled 46 | )); 47 | } 48 | 49 | /** 50 | * Align the back button behavior with Android S 51 | * where moving root activities to background instead of finishing activities. 52 | * @see onBackPressed 53 | */ 54 | @Override 55 | public void invokeDefaultOnBackPressed() { 56 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { 57 | if (!moveTaskToBack(false)) { 58 | // For non-root activities, use the default implementation to finish them. 59 | super.invokeDefaultOnBackPressed(); 60 | } 61 | return; 62 | } 63 | 64 | // Use the default back button implementation on Android S 65 | // because it's doing more than {@link Activity#moveTaskToBack} in fact. 66 | super.invokeDefaultOnBackPressed(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /components/MonthlyBudgetCategory.tsx: -------------------------------------------------------------------------------- 1 | import { Box, HStack, Text, useTheme, VStack } from "native-base"; 2 | import React from "react"; 3 | import { getCategoryIcon } from "../utils/getCategoryIcon"; 4 | import EZProgress from "./shared/EZProgress"; 5 | import { Budget } from "../interfaces/Budget"; 6 | import { AntDesign } from "@expo/vector-icons"; 7 | import COLORS from "../colors"; 8 | import { useSelector } from "react-redux"; 9 | import { RootState } from "../redux/store"; 10 | 11 | interface MonthlyBudgetCategoryProps { 12 | budget: Budget; 13 | monthlyTotal: number; 14 | } 15 | 16 | const MonthlyBudgetCategory: React.FC = ({ budget, monthlyTotal }) => { 17 | const { budget: amount, category, color } = budget; 18 | const user: any = useSelector((state: RootState) => state.user); 19 | const { 20 | colors: { muted }, 21 | } = useTheme(); 22 | 23 | const budgetStatus = () => { 24 | const threshold = (amount * 75) / 100; 25 | 26 | let statusMessage = ""; 27 | 28 | if (monthlyTotal >= threshold && monthlyTotal < amount) { 29 | statusMessage = "Almost exceeding"; 30 | } else if (monthlyTotal >= amount) { 31 | statusMessage = "Budget exceeded !"; 32 | } 33 | 34 | if (statusMessage) { 35 | return ( 36 | 37 | 38 | {statusMessage} 39 | 40 | ); 41 | } 42 | 43 | return null; 44 | }; 45 | 46 | return ( 47 | 58 | 59 | 60 | 67 | {getCategoryIcon(category, 24, muted[50])} 68 | 69 | 70 | {category} 71 | 72 | 73 | 74 | 75 | 76 | {monthlyTotal.toFixed(2)} {user.symbol} /{" "} 77 | {Number.isInteger(amount) ? amount : amount.toFixed(2)} {user.symbol} 78 | 79 | 80 | 81 | 82 | {/* {isAlmostExceeded() && ( 83 | 84 | 89 | Almost exceeded 90 | 91 | )} */} 92 | {budgetStatus()} 93 | 94 | ); 95 | }; 96 | 97 | export default MonthlyBudgetCategory; 98 | -------------------------------------------------------------------------------- /components/LoginProviders.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { makeRedirectUri, startAsync } from "expo-auth-session"; 3 | import { Google, Facebook } from "../assets/SVG"; 4 | import { HStack, Pressable } from "native-base"; 5 | import { supabase } from "../api/supabase"; 6 | import { SUPABASE_URL } from "@env"; 7 | import * as WebBrowser from "expo-web-browser"; 8 | 9 | const LoginProviders: React.FC = () => { 10 | const googleSignIn = async () => { 11 | const redirectUrl = makeRedirectUri({ 12 | path: "/auth/callback", 13 | }); 14 | 15 | const authResponse = await startAsync({ 16 | authUrl: `${SUPABASE_URL}/auth/v1/authorize?provider=google&redirect_to=${redirectUrl}`, 17 | returnUrl: redirectUrl, 18 | }); 19 | 20 | if (authResponse.type === "success") { 21 | supabase.auth.setSession({ 22 | access_token: authResponse.params.access_token, 23 | refresh_token: authResponse.params.refresh_token, 24 | }); 25 | } 26 | }; 27 | 28 | const extractParamsFromUrl = (url: string) => { 29 | const params = new URLSearchParams(url.split("#")[1]); 30 | const data = { 31 | access_token: params.get("access_token"), 32 | expires_in: parseInt(params.get("expires_in") || "0"), 33 | refresh_token: params.get("refresh_token"), 34 | token_type: params.get("token_type"), 35 | provider_token: params.get("provider_token"), 36 | }; 37 | 38 | return data; 39 | }; 40 | 41 | useEffect(() => { 42 | WebBrowser.warmUpAsync(); 43 | 44 | return () => { 45 | WebBrowser.coolDownAsync(); 46 | }; 47 | }, []); 48 | 49 | // const doSmth = async () => { 50 | // //const url = (await googleSignIn()) as string; 51 | 52 | // console.log(url); 53 | // const returnUrl = makeRedirectUri({ 54 | // path: "/", // This were missing if you use react-navigation linking 55 | // useProxy: false, 56 | // }); 57 | 58 | // const result = await WebBrowser.openAuthSessionAsync(url, returnUrl, { 59 | // showInRecents: true, 60 | // }); 61 | 62 | // if (result.type === "success") { 63 | // const data = extractParamsFromUrl(result.url); 64 | // console.log(data); 65 | // } 66 | // }; 67 | 68 | // const googleSignIn = async () => { 69 | // const result = await supabase.auth.signInWithOAuth({ 70 | // provider: "google", 71 | // options: { 72 | // redirectTo: "wwphlzjxrnbilstgyzge://google-auth", 73 | // }, 74 | // }); 75 | // return result.data.url; 76 | // }; 77 | 78 | return ( 79 | 80 | 89 | 90 | 91 | 92 | 100 | 101 | 102 | 103 | ); 104 | }; 105 | 106 | export default LoginProviders; 107 | -------------------------------------------------------------------------------- /navigation/StackNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 2 | import { useEffect, useState } from "react"; 3 | import { useSelector } from "react-redux"; 4 | import { AppStackParamList, StackConfig } from "../interfaces/Navigation"; 5 | import { RootState } from "../redux/store"; 6 | import AddCurrencyScreen from "../screens/AddCurrencyScreen"; 7 | import AddExpenseScreen from "../screens/AddExpenseScreen"; 8 | import CategoryExpensesScreen from "../screens/CategoryExpensesScreen"; 9 | import EditBudgetScreen from "../screens/EditBudgetsScreen"; 10 | import LoginScreen from "../screens/LoginScreen"; 11 | import OnboardingScreen from "../screens/OnboardingScreen"; 12 | import RegisterScreen from "../screens/RegisterScreen"; 13 | import TabNavigator from "./TabNavigator"; 14 | import ResetPasswordScreen from "../screens/ResetPasswordScreen"; 15 | import AboutScreen from "../screens/AboutScreen"; 16 | import ChangePasswordScreen from "../screens/ChangePasswordScreen"; 17 | 18 | const Stack = createNativeStackNavigator(); 19 | 20 | const StackNavigator: React.FC = () => { 21 | const [initialScreen, setInitialScreen] = useState( 22 | undefined 23 | ); 24 | const { onboarded } = useSelector((state: RootState) => state.onboard); 25 | const user: any = useSelector((state: RootState) => state.user); 26 | 27 | useEffect(() => { 28 | const checkOnboarding = async () => { 29 | if (!onboarded) { 30 | setInitialScreen("Onboarding"); 31 | } else { 32 | if (user.email && user.currency) { 33 | setInitialScreen("Tabs"); 34 | } else if (user.email && !user.currency) { 35 | setInitialScreen("Currency"); 36 | } else { 37 | setInitialScreen("Login"); 38 | } 39 | } 40 | }; 41 | checkOnboarding(); 42 | }, []); 43 | 44 | if (!initialScreen) { 45 | return null; 46 | } 47 | 48 | const routes: StackConfig[] = [ 49 | { name: "Onboarding", component: OnboardingScreen, options: { headerShown: false } }, 50 | { name: "Login", component: LoginScreen, options: { headerShown: false } }, 51 | { name: "Register", component: RegisterScreen, options: { headerShown: false } }, 52 | { name: "ResetPassword", component: ResetPasswordScreen, options: { headerShown: false } }, 53 | { name: "Tabs", component: TabNavigator, options: { headerShown: false } }, 54 | { name: "Currency", component: AddCurrencyScreen, options: { headerShown: false } }, 55 | { name: "About", component: AboutScreen, options: { headerShown: false } }, 56 | { name: "ChangePassword", component: ChangePasswordScreen, options: { headerShown: false } }, 57 | { 58 | name: "CategoryExpenses", 59 | component: CategoryExpensesScreen, 60 | options: { headerTintColor: "#fff", headerBackTitleVisible: false }, 61 | }, 62 | { 63 | name: "AddExpense", 64 | component: AddExpenseScreen, 65 | options: { presentation: "containedModal", headerShown: false }, 66 | }, 67 | { 68 | name: "EditBudgets", 69 | component: EditBudgetScreen, 70 | options: { presentation: "containedModal", headerShown: false }, 71 | }, 72 | ]; 73 | 74 | return ( 75 | 76 | {routes.map((route: StackConfig, key) => ( 77 | 78 | ))} 79 | 80 | ); 81 | }; 82 | 83 | export default StackNavigator; 84 | -------------------------------------------------------------------------------- /components/shared/EZInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { ForwardedRef, forwardRef } from "react"; 2 | import { KeyboardTypeOptions, ReturnKeyTypeOptions } from "react-native"; 3 | import { FormControl, Input, Text, useTheme } from "native-base"; 4 | import { FontAwesome } from "@expo/vector-icons"; 5 | import COLORS from "../../colors"; 6 | import { useSelector } from "react-redux"; 7 | import { RootState } from "../../redux/store"; 8 | 9 | type inputType = "text" | "password" | undefined; 10 | 11 | interface EZInputProps { 12 | placeholder?: string; 13 | value: string | undefined; 14 | onChangeText: (e: string) => void; 15 | error?: any; 16 | type: inputType; 17 | textContentType?: any; 18 | InputRightElement?: JSX.Element; 19 | keyboardType?: KeyboardTypeOptions | undefined; 20 | returnKeyType?: ReturnKeyTypeOptions | undefined; 21 | multiline?: boolean; 22 | numberOfLines?: number; 23 | onFocus?: any; 24 | ref?: any; 25 | onSubmitEditing?: () => void; 26 | alignItems: string; 27 | flex?: number | string; 28 | formHeight?: number | string; 29 | label?: string; 30 | } 31 | 32 | interface EZInputStylesProp { 33 | [key: string]: any; 34 | } 35 | 36 | type Props = EZInputProps & EZInputStylesProp; 37 | 38 | const EZInput = forwardRef((props: Props, ref: ForwardedRef) => { 39 | const { 40 | colors: { muted }, 41 | } = useTheme(); 42 | const user = useSelector((state: RootState) => state.user); 43 | 44 | const { 45 | placeholder, 46 | error, 47 | value, 48 | onChangeText, 49 | type, 50 | textContentType, 51 | InputRightElement, 52 | keyboardType, 53 | multiline, 54 | numberOfLines, 55 | onFocus, 56 | returnKeyType, 57 | onSubmitEditing, 58 | alignItems, 59 | flex, 60 | formHeight, 61 | label, 62 | } = props; 63 | return ( 64 | 71 | {label && ( 72 | 73 | {label} 74 | 75 | )} 76 | 77 | 100 | }> 103 | {error} 104 | 105 | 106 | ); 107 | }); 108 | 109 | export default EZInput; 110 | -------------------------------------------------------------------------------- /android/app/src/debug/java/com/vladi/ExpenseZen/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.vladi.ExpenseZen; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 21 | import com.facebook.react.ReactInstanceEventListener; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | /** 28 | * Class responsible of loading Flipper inside your React Native application. This is the debug 29 | * flavor of it. Here you can add your own plugins and customize the Flipper setup. 30 | */ 31 | public class ReactNativeFlipper { 32 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 33 | if (FlipperUtils.shouldEnableFlipper(context)) { 34 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 35 | 36 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 37 | client.addPlugin(new DatabasesFlipperPlugin(context)); 38 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 39 | client.addPlugin(CrashReporterPlugin.getInstance()); 40 | 41 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 42 | NetworkingModule.setCustomClientBuilder( 43 | new NetworkingModule.CustomClientBuilder() { 44 | @Override 45 | public void apply(OkHttpClient.Builder builder) { 46 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 47 | } 48 | }); 49 | client.addPlugin(networkFlipperPlugin); 50 | client.start(); 51 | 52 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 53 | // Hence we run if after all native modules have been initialized 54 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 55 | if (reactContext == null) { 56 | reactInstanceManager.addReactInstanceEventListener( 57 | new ReactInstanceEventListener() { 58 | @Override 59 | public void onReactContextInitialized(ReactContext reactContext) { 60 | reactInstanceManager.removeReactInstanceEventListener(this); 61 | reactContext.runOnNativeModulesQueueThread( 62 | new Runnable() { 63 | @Override 64 | public void run() { 65 | client.addPlugin(new FrescoFlipperPlugin()); 66 | } 67 | }); 68 | } 69 | }); 70 | } else { 71 | client.addPlugin(new FrescoFlipperPlugin()); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /screens/AboutScreen.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "expo-status-bar"; 2 | import { Text, useTheme, VStack } from "native-base"; 3 | import React, { Fragment } from "react"; 4 | import { ScrollView, SafeAreaView, TouchableOpacity } from "react-native"; 5 | import { Ionicons } from "@expo/vector-icons"; 6 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 7 | import { useSelector } from "react-redux"; 8 | import { RootState } from "../redux/store"; 9 | 10 | interface AboutScreenProps { 11 | navigation: NavigationProp; 12 | } 13 | 14 | interface textInfo { 15 | title: string; 16 | content: JSX.Element; 17 | } 18 | 19 | const paragraphs: textInfo[] = [ 20 | { 21 | title: " About ExpenseZen", 22 | content: ( 23 | 24 | Welcome to ExpenseZen, your go-to solution for effortless expense tracking and budget 25 | management. 26 | 27 | ), 28 | }, 29 | { 30 | title: " Key Features:", 31 | content: ( 32 | 33 | 34 | {`\u2022`}{" "} 35 | 36 | Easy Expense Tracking:{" "} 37 | 38 | 39 | Record your daily expenses quickly and hassle-free. 40 | 41 | 42 | 43 | {`\u2022`}{" "} 44 | 45 | Smart Budgets:{" "} 46 | 47 | 48 | Set up monthly budgets for various spending categories. 49 | 50 | 51 | 52 | {`\u2022`}{" "} 53 | 54 | Clear Visuals:{" "} 55 | 56 | 57 | Gain insights with a simple pie chart that illustrates your spending distribution. 58 | 59 | 60 | 61 | ), 62 | }, 63 | { 64 | title: " Why ExpenseZen:", 65 | content: ( 66 | 67 | Tracking your expenses shouldn't be complicated. ExpenseZen keeps it simple, focusing on 68 | what truly matters - helping you stay on top of your finances. 69 | 70 | ), 71 | }, 72 | { 73 | title: " Goal:", 74 | content: ( 75 | 76 | ExpenseZen has the goal is aiming to simplify expense management, allowing you to make 77 | informed financial decisions without the fuss. 78 | 79 | ), 80 | }, 81 | ]; 82 | 83 | const AboutScreen: React.FC = ({ navigation }) => { 84 | const { 85 | colors: { muted }, 86 | } = useTheme(); 87 | const { theme } = useSelector((state: RootState) => state.user); 88 | 89 | return ( 90 | 91 | 92 | 93 | 94 | navigation.goBack()}> 95 | 96 | 97 | {paragraphs.map((item, key) => ( 98 | 99 | 100 | {item.title} 101 | 102 | {item.content} 103 | 104 | ))} 105 | 106 | 107 | 108 | ); 109 | }; 110 | 111 | export default AboutScreen; 112 | -------------------------------------------------------------------------------- /assets/SVG/NoChartData.svg: -------------------------------------------------------------------------------- 1 | void -------------------------------------------------------------------------------- /screens/AddCurrencyScreen.tsx: -------------------------------------------------------------------------------- 1 | import { Text, View, VStack } from "native-base"; 2 | import { SafeAreaView, StyleSheet } from "react-native"; 3 | import React, { useState, useEffect } from "react"; 4 | import EZButton from "../components/shared/EZButton"; 5 | import COLORS from "../colors"; 6 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 7 | import { CurrencyService } from "../api/services/CurrencyService"; 8 | import { useSelector, useDispatch } from "react-redux"; 9 | import { RootState } from "../redux/store"; 10 | import { setCurrency } from "../redux/userReducer"; 11 | import RNPickerSelect from "react-native-picker-select"; 12 | 13 | interface AddCurrencyScreenProps { 14 | navigation: NavigationProp; 15 | } 16 | 17 | const AddCurrencyScreen: React.FC = ({ navigation }) => { 18 | const [currencies, setCurrencies] = useState([]); 19 | const [selectedCurrency, setSelectedCurrency] = useState(); 20 | const [loading, setLoading] = useState(); 21 | const { id } = useSelector((state: RootState) => state.user); 22 | 23 | const dispatch = useDispatch(); 24 | 25 | useEffect(() => { 26 | getCurrencies(); 27 | }, [navigation]); 28 | 29 | const getCurrencies = async () => { 30 | const data = await CurrencyService.getAllCurrencies(); 31 | 32 | let currenciesArray = []; 33 | 34 | for (const item in data) { 35 | currenciesArray.push({ 36 | label: `${data[item].symbol_native} ${data[item].code}`, 37 | value: `${data[item].symbol_native} ${data[item].code}`, 38 | }); 39 | } 40 | 41 | setCurrencies(currenciesArray); 42 | }; 43 | 44 | const saveCurrency = async () => { 45 | setLoading(true); 46 | 47 | const currencyText = selectedCurrency.split(" "); 48 | 49 | const currencySymbol = currencyText[0]; 50 | const currencyCode = currencyText[1]; 51 | 52 | await CurrencyService.updateUserCurrency(id, currencySymbol, currencyCode); 53 | 54 | const payload = { 55 | name: currencyCode, 56 | symbol: currencySymbol, 57 | }; 58 | 59 | dispatch(setCurrency(payload)); 60 | 61 | setLoading(false); 62 | 63 | goToTabs(); 64 | }; 65 | 66 | const goToTabs = () => { 67 | navigation.navigate("Tabs"); 68 | }; 69 | 70 | return ( 71 | 72 | 73 | 74 | 75 | Select your currency 76 | 77 | setSelectedCurrency(value)} 81 | items={currencies} 82 | /> 83 | 84 | 85 | 97 | SAVE 98 | 99 | 100 | 101 | ); 102 | }; 103 | 104 | export default AddCurrencyScreen; 105 | 106 | const pickerSelectStyles = StyleSheet.create({ 107 | inputIOS: { 108 | color: COLORS.PURPLE[700], 109 | width: "80%", 110 | alignSelf: "center", 111 | fontSize: 16, 112 | paddingVertical: 12, 113 | paddingHorizontal: 10, 114 | borderWidth: 1, 115 | borderColor: COLORS.MUTED[200], 116 | backgroundColor: COLORS.MUTED[200], 117 | borderRadius: 12, 118 | }, 119 | inputAndroid: { 120 | color: COLORS.PURPLE[700], 121 | width: "80%", 122 | alignSelf: "center", 123 | fontSize: 16, 124 | paddingVertical: 12, 125 | paddingHorizontal: 10, 126 | borderWidth: 1, 127 | borderColor: COLORS.MUTED[200], 128 | backgroundColor: COLORS.MUTED[200], 129 | borderRadius: 12, 130 | }, 131 | }); 132 | -------------------------------------------------------------------------------- /components/ChangePasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigation } from "@react-navigation/native"; 2 | import { NativeStackNavigationProp } from "@react-navigation/native-stack"; 3 | import { Text, useTheme, VStack } from "native-base"; 4 | import React, { useState } from "react"; 5 | import { Alert, TouchableOpacity } from "react-native"; 6 | import { AppStackParamList } from "../interfaces/Navigation"; 7 | import { Ionicons } from "@expo/vector-icons"; 8 | import COLORS from "../colors"; 9 | import EZInput from "./shared/EZInput"; 10 | import { useFormik } from "formik"; 11 | import { authInput } from "../commonStyles"; 12 | import EZButton from "./shared/EZButton"; 13 | import { changePasswordSchema } from "../schemas/changePasswordSchemta"; 14 | import { UserService } from "../api/services/UserService"; 15 | import { useSelector } from "react-redux"; 16 | import { RootState } from "../redux/store"; 17 | 18 | const ChangePasswordForm: React.FC = () => { 19 | const navigation = useNavigation>(); 20 | const [loading, setLoading] = useState(false); 21 | const user: any = useSelector((state: RootState) => state.user); 22 | const { 23 | colors: { muted }, 24 | } = useTheme(); 25 | 26 | const formik = useFormik({ 27 | initialValues: { 28 | password: "", 29 | repeatPassword: "", 30 | }, 31 | validationSchema: changePasswordSchema, 32 | onSubmit: async (values) => { 33 | const { password } = values; 34 | 35 | try { 36 | const response = await UserService.changePassword(user.email, password); 37 | 38 | if (response?.message) { 39 | Alert.alert("Success", response.message, [ 40 | { 41 | text: "OK", 42 | onPress: () => { 43 | formik.resetForm(); 44 | navigation.goBack(); 45 | }, 46 | }, 47 | ]); 48 | } 49 | } catch (error) { 50 | console.log(error); 51 | } 52 | }, 53 | }); 54 | 55 | const handleValue = (label: string, value: string) => { 56 | formik.setFieldValue(label, value); 57 | }; 58 | 59 | const { values, errors, touched, submitForm } = formik; 60 | 61 | const submit = async () => { 62 | setLoading(true); 63 | await submitForm(); 64 | setLoading(false); 65 | }; 66 | 67 | return ( 68 | 69 | navigation.goBack()}> 70 | 71 | 72 | 73 | 74 | Change your password 75 | 76 | 77 | Fill the info to change your password 78 | 79 | 80 | 81 | handleValue("password", e)} 87 | error={touched.password && errors.password} 88 | returnKeyType="next" 89 | borderRadius={12} 90 | borderColor="muted.200" 91 | /> 92 | handleValue("repeatPassword", e)} 99 | error={touched.repeatPassword && errors.repeatPassword} 100 | returnKeyType="next" 101 | borderRadius={12} 102 | borderColor="muted.200" 103 | /> 104 | 105 | 114 | Change password 115 | 116 | 117 | ); 118 | }; 119 | 120 | export default ChangePasswordForm; 121 | -------------------------------------------------------------------------------- /screens/CategoryExpensesScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect } from "react"; 2 | import { SafeAreaView, TouchableOpacity } from "react-native"; 3 | import { Text, ScrollView, HStack, View, useTheme } from "native-base"; 4 | import { NavigationProp, ParamListBase, RouteProp } from "@react-navigation/native"; 5 | import { FontAwesome5 } from "@expo/vector-icons"; 6 | 7 | import { Expense } from "../interfaces/Expense"; 8 | import moment from "moment"; 9 | import { StatusBar } from "expo-status-bar"; 10 | import { getCategoryIcon } from "../utils/getCategoryIcon"; 11 | import COLORS from "../colors"; 12 | import { AppStackParamList } from "../interfaces/Navigation"; 13 | import { useSelector } from "react-redux"; 14 | import { RootState } from "../redux/store"; 15 | 16 | type Props = { 17 | navigation: NavigationProp; 18 | route: RouteProp; 19 | }; 20 | 21 | const CategoryExpensesScreen: React.FC = ({ navigation, route }) => { 22 | const { params } = route; 23 | const { expenses, name } = params; 24 | const { theme } = useSelector((state: RootState) => state.user); 25 | const { 26 | colors: { muted }, 27 | } = useTheme(); 28 | 29 | useLayoutEffect(() => { 30 | navigation.setOptions({ 31 | headerStyle: { 32 | borderBottomWidth: 0, 33 | }, 34 | headerShadowVisible: false, 35 | headerTitle: () => ( 36 | 37 | {name} 38 | 39 | ), 40 | headerLeft: () => ( 41 | navigation.goBack()}> 42 | 43 | 44 | ), 45 | }); 46 | }, [navigation]); 47 | 48 | return ( 49 | 50 | 51 | 52 | {expenses.map((expense: Expense, index: number) => { 53 | const parsedDate = moment(expense.payDate, "YYYY-MM-DD"); 54 | const formattedDate = parsedDate.format("D MMMM"); 55 | 56 | return ( 57 | 58 | 59 | 60 | {getCategoryIcon(expense.name, 24, muted[900])} 61 | 62 | 63 | 69 | {index !== expenses.length - 1 && ( 70 | 79 | )} 80 | 81 | 91 | 92 | 93 | 100 | 101 | 102 | # {index + 1} 103 | 104 | 105 | {expense.description} 106 | 107 | 108 | {formattedDate} 109 | 110 | 111 | 112 | 113 | {expense.amount.toFixed(2)} $ 114 | 115 | 116 | 117 | 118 | 119 | ); 120 | })} 121 | 122 | 123 | ); 124 | }; 125 | 126 | export default CategoryExpensesScreen; 127 | -------------------------------------------------------------------------------- /components/ResetPasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import { Text, useTheme, VStack } from "native-base"; 2 | import React, { useState } from "react"; 3 | import { TouchableOpacity, Alert } from "react-native"; 4 | import COLORS from "../colors"; 5 | import EZInput from "./shared/EZInput"; 6 | import { authInput } from "../commonStyles"; 7 | import EZButton from "./shared/EZButton"; 8 | import { Ionicons } from "@expo/vector-icons"; 9 | import { useNavigation } from "@react-navigation/native"; 10 | import { NativeStackNavigationProp } from "@react-navigation/native-stack"; 11 | import { AppStackParamList } from "../interfaces/Navigation"; 12 | import { useFormik } from "formik"; 13 | import { resetPasswordSchema } from "../schemas/resetPasswordScheama"; 14 | import { UserService } from "../api/services/UserService"; 15 | 16 | const ResetPasswordForm: React.FC = () => { 17 | const navigation = useNavigation>(); 18 | const [loading, setLoading] = useState(false); 19 | const { 20 | colors: { muted }, 21 | } = useTheme(); 22 | 23 | const formik = useFormik({ 24 | initialValues: { 25 | email: "", 26 | password: "", 27 | repeatPassword: "", 28 | }, 29 | validationSchema: resetPasswordSchema, 30 | onSubmit: async (values) => { 31 | const { email, password } = values; 32 | 33 | try { 34 | const response = await UserService.resetPassword(email, password); 35 | 36 | if (response?.message === "This user does not exist !") { 37 | Alert.alert("Error", response.message, [{ text: "OK", style: "destructive" }]); 38 | } else if (response?.message === "Password resetted succesfully !") { 39 | Alert.alert("Success", response.message, [ 40 | { 41 | text: "OK", 42 | onPress: () => { 43 | formik.resetForm(); 44 | navigation.goBack(); 45 | }, 46 | }, 47 | ]); 48 | } 49 | } catch (error) { 50 | console.log(error); 51 | } 52 | }, 53 | }); 54 | 55 | const { values, errors, submitForm, touched } = formik; 56 | 57 | const handleValue = (label: string, value: string) => { 58 | formik.setFieldValue(label, value); 59 | }; 60 | 61 | const submit = async () => { 62 | setLoading(true); 63 | await submitForm(); 64 | setLoading(false); 65 | }; 66 | 67 | return ( 68 | 69 | navigation.goBack()}> 70 | 71 | 72 | 73 | 74 | Reset your password 75 | 76 | 77 | Fill the info to change your password 78 | 79 | 80 | 81 | handleValue("email", e)} 87 | error={touched.email && errors.email} 88 | returnKeyType="next" 89 | borderRadius={12} 90 | borderColor="muted.200" 91 | /> 92 | handleValue("password", e)} 99 | error={touched.password && errors.password} 100 | returnKeyType="next" 101 | borderRadius={12} 102 | borderColor="muted.200" 103 | /> 104 | handleValue("repeatPassword", e)} 111 | error={touched.repeatPassword && errors.repeatPassword} 112 | returnKeyType="next" 113 | placeholder="" 114 | borderRadius={12} 115 | borderColor="muted.200" 116 | /> 117 | 118 | 127 | Reset password 128 | 129 | 130 | ); 131 | }; 132 | 133 | export default ResetPasswordForm; 134 | -------------------------------------------------------------------------------- /navigation/TabNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; 2 | import { Fragment, useRef, useEffect } from "react"; 3 | import COLORS from "../colors"; 4 | import { TAB_BAR_HEIGHT } from "../constants/NavigationConstants"; 5 | import HomeScreen from "../screens/HomeScreen"; 6 | import { Ionicons } from "@expo/vector-icons"; 7 | import GraphScreen from "../screens/GraphScreen"; 8 | import CategoriesScreen from "../screens/CategoriesScreen"; 9 | import SettingsScreen from "../screens/SettingsScreen"; 10 | import { Animated, useWindowDimensions } from "react-native"; 11 | import EZHeaderBackground from "../components/shared/EZHeaderBackground"; 12 | import { useSelector } from "react-redux"; 13 | import { RootState } from "../redux/store"; 14 | 15 | const Tab = createBottomTabNavigator(); 16 | 17 | const TabNavigator: React.FC = () => { 18 | const { width } = useWindowDimensions(); 19 | const tabOffsetValue = useRef(new Animated.Value(0)).current; 20 | const user = useSelector((state: RootState) => state.user); 21 | 22 | const tabWidth = width / 4; 23 | 24 | const animateTabOffset = (index: number) => { 25 | Animated.spring(tabOffsetValue, { 26 | toValue: tabWidth * index, 27 | speed: 20, 28 | useNativeDriver: true, 29 | }).start(); 30 | }; 31 | 32 | const resetOffset = () => { 33 | Animated.spring(tabOffsetValue, { 34 | toValue: 0, 35 | speed: 0, 36 | useNativeDriver: true, 37 | }).start(); 38 | }; 39 | 40 | useEffect(() => { 41 | resetOffset(); 42 | }, [user.id]); 43 | 44 | return ( 45 | 46 | , 59 | }}> 60 | ( 66 | 71 | ), 72 | }} 73 | listeners={() => ({ 74 | tabPress: () => animateTabOffset(0), 75 | })} 76 | /> 77 | ( 83 | 88 | ), 89 | }} 90 | listeners={() => ({ 91 | tabPress: () => animateTabOffset(1), 92 | })} 93 | /> 94 | ( 100 | 105 | ), 106 | }} 107 | listeners={() => ({ 108 | tabPress: () => animateTabOffset(2), 109 | })} 110 | /> 111 | ( 117 | 122 | ), 123 | }} 124 | listeners={() => ({ 125 | tabPress: () => animateTabOffset(3), 126 | })} 127 | /> 128 | 129 | 140 | 141 | ); 142 | }; 143 | 144 | export default TabNavigator; 145 | -------------------------------------------------------------------------------- /screens/EditBudgetsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 2 | import { Text, useTheme, VStack } from "native-base"; 3 | import React, { useState } from "react"; 4 | import { SafeAreaView, TouchableOpacity } from "react-native"; 5 | import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; 6 | import { AntDesign } from "@expo/vector-icons"; 7 | import MonthlyBudgetItem from "../components/MonthlyBudgetItem"; 8 | import { Category } from "../interfaces/Category"; 9 | import { getCategoryIcon } from "../utils/getCategoryIcon"; 10 | import { UserService } from "../api/services/UserService"; 11 | import { useDispatch, useSelector } from "react-redux"; 12 | import { RootState } from "../redux/store"; 13 | import { Budget } from "../interfaces/Budget"; 14 | import EZButton from "../components/shared/EZButton"; 15 | import COLORS from "../colors"; 16 | import { StatusBar } from "expo-status-bar"; 17 | import { 18 | categoriesSelector, 19 | editBudgetsAction, 20 | monthlyBudgetsSelector, 21 | } from "../redux/expensesReducers"; 22 | 23 | interface EditBudgetScreenProps { 24 | navigation: NavigationProp; 25 | } 26 | 27 | const EditBudgetScreen: React.FC = ({ navigation }) => { 28 | const user = useSelector((state: RootState) => state.user); 29 | const [buttonLoading, setButtonLoading] = useState(false); 30 | const [budgets, setBudgets] = useState([]); 31 | const dispatch = useDispatch(); 32 | const monthlyBudgets = useSelector(monthlyBudgetsSelector); 33 | const categories = useSelector(categoriesSelector); 34 | const { 35 | colors: { muted }, 36 | } = useTheme(); 37 | 38 | const closeModal = () => { 39 | navigation.goBack(); 40 | }; 41 | 42 | const handleValues = (value: string, category: Category) => { 43 | const element = budgets.find((item: Budget) => item.category === category.name); 44 | 45 | let newValues; 46 | const formatAmount = value.replace(",", "."); 47 | const numericFormat = Number(formatAmount); 48 | 49 | if (element) { 50 | newValues = budgets.map((item: any) => 51 | item === element ? { ...item, budget: numericFormat } : item 52 | ); 53 | } else { 54 | newValues = [ 55 | ...budgets, 56 | { 57 | budget: numericFormat, 58 | category: category.name, 59 | color: category.color, 60 | id: category.id, 61 | }, 62 | ]; 63 | } 64 | 65 | setBudgets(newValues); 66 | }; 67 | 68 | const saveBudgets = async () => { 69 | setButtonLoading(true); 70 | await UserService.saveUserBudgets(user.id, budgets); 71 | dispatch(editBudgetsAction(budgets)); 72 | setButtonLoading(false); 73 | navigation.goBack(); 74 | }; 75 | 76 | const budgetValues: Budget[] = []; 77 | 78 | const budgetCategories = categories!.map((category: Category) => { 79 | const budget = monthlyBudgets.find((item: Budget) => item.category === category.name); 80 | 81 | budgetValues.push( 82 | budget 83 | ? { ...budget, categoryId: category.id } 84 | : { budget: 0, category: category.name, categoryId: category.id } 85 | ); 86 | 87 | return { 88 | id: category.id, 89 | name: category.name, 90 | color: category.color, 91 | icon: getCategoryIcon(category.name, 24, muted[50]), 92 | budget: budget ? budget.budget : 0, 93 | }; 94 | }); 95 | 96 | return ( 97 | 98 | 99 | 100 | 101 | 109 | 110 | 111 | 112 | Edit your monthly budgets 113 | 114 | {budgetCategories.map((category: Category, index: number) => ( 115 | handleValues(e, category)} 119 | /> 120 | ))} 121 | 122 | 135 | SAVE 136 | 137 | 138 | 139 | 140 | ); 141 | }; 142 | 143 | export default EditBudgetScreen; 144 | -------------------------------------------------------------------------------- /redux/expensesReducers.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, createSlice } from "@reduxjs/toolkit"; 2 | import moment from "moment"; 3 | import { Budget } from "../interfaces/Budget"; 4 | import { Category } from "../interfaces/Category"; 5 | import { Expense } from "../interfaces/Expense"; 6 | import { RootState } from "./store"; 7 | 8 | const todayDate = moment().format("YYYY-MM-DD"); 9 | 10 | interface initialStateProps { 11 | expenses: Expense[]; 12 | todayTotal: number; 13 | topSpendingCategories: Category[]; 14 | categories: Category[]; 15 | budgets: Budget[]; 16 | } 17 | 18 | const initialState: initialStateProps = { 19 | expenses: [], 20 | todayTotal: 0, 21 | topSpendingCategories: [], 22 | categories: [], 23 | budgets: [], 24 | }; 25 | 26 | const expensesReducer = createSlice({ 27 | name: "expenses", 28 | initialState: initialState, 29 | reducers: { 30 | setExpenses: (state, action) => { 31 | state.expenses = action.payload; 32 | }, 33 | 34 | addExpense: (state, action) => { 35 | state.expenses = [...state.expenses, action.payload]; 36 | }, 37 | setCategories: (state, action) => { 38 | state.categories = action.payload; 39 | }, 40 | setTopSpedingCategories: (state, action) => { 41 | state.topSpendingCategories = action.payload; 42 | }, 43 | setBudgets: (state, action) => { 44 | state.budgets = action.payload; 45 | }, 46 | editBudgets: (state, action) => { 47 | let budgets = action.payload; 48 | 49 | state.budgets = state.budgets.map((budget) => { 50 | const budgetToEdit = budgets.find((item: Budget) => item.category === budget.category); 51 | 52 | if (budgetToEdit) { 53 | return { 54 | ...budget, 55 | budget: budgetToEdit.budget, 56 | }; 57 | } else { 58 | return budget; 59 | } 60 | }); 61 | 62 | let budgetsToAdd: Budget[] = budgets.filter((item: any) => { 63 | const existentBudget = state.budgets.find((budget) => budget.category === item.category); 64 | 65 | if (!existentBudget) { 66 | return true; 67 | } else { 68 | return false; 69 | } 70 | }); 71 | 72 | if (budgetsToAdd && budgetsToAdd.length > 0) { 73 | state.budgets = [...state.budgets, ...budgetsToAdd]; 74 | } 75 | }, 76 | }, 77 | }); 78 | 79 | //actions 80 | export const setExpensesAction = expensesReducer.actions.setExpenses; 81 | export const addExpenseAction = expensesReducer.actions.addExpense; 82 | export const setCategoriesAction = expensesReducer.actions.setCategories; 83 | export const setBudgetsAction = expensesReducer.actions.setBudgets; 84 | export const editBudgetsAction = expensesReducer.actions.editBudgets; 85 | 86 | //selectors 87 | const generalState = (state: RootState) => state.expenses; 88 | const globalState = (state: RootState) => state; 89 | 90 | export const todayTotalSelector = createSelector([generalState], (expenses: any) => { 91 | return expenses.expenses 92 | .filter((expense: Expense) => expense.payDate === todayDate) 93 | .reduce((accumulator: any, currentValue: Expense) => accumulator + currentValue.amount, 0); 94 | }); 95 | 96 | export const monthTotalSelector = createSelector([globalState], (globalState: any) => { 97 | const currentMonth = globalState.user.month; 98 | const parsedMonth = moment(currentMonth, "MMMM"); 99 | if (parsedMonth.isValid()) { 100 | const monthNumber = parsedMonth.month(); 101 | const startOfMonth = moment().month(monthNumber).startOf("month").format("YYYY-MM-DD"); 102 | const endOfMonth = moment().month(monthNumber).endOf("month").format("YYYY-MM-DD"); 103 | return globalState.expenses.expenses 104 | .filter( 105 | (expense: Expense) => expense.payDate >= startOfMonth && expense.payDate <= endOfMonth 106 | ) 107 | .reduce((accumulator: any, currentValue: Expense) => accumulator + currentValue.amount, 0); 108 | } 109 | }); 110 | 111 | export const categoriesSelector = createSelector([generalState], (expenses: any) => { 112 | return expenses.categories; 113 | }); 114 | 115 | export const topSpedingCategoriesSelector = createSelector([generalState], (expenses: any) => { 116 | const topSpendingCategories = expenses.expenses.reduce((accumulator: any, expense: any) => { 117 | const categoryName = expense.name; 118 | const existingCategory = accumulator.find((item: any) => item.name === categoryName); 119 | 120 | if (!existingCategory) { 121 | const { color, id } = expenses.categories.find( 122 | (item: Category) => item.name === categoryName 123 | ); 124 | accumulator.push({ 125 | name: categoryName, 126 | amount: expense.amount, 127 | color, 128 | id, 129 | }); 130 | } else { 131 | existingCategory.amount += expense.amount; 132 | } 133 | 134 | return accumulator; 135 | }, []); 136 | 137 | return topSpendingCategories.sort((a: Category, b: Category) => b.amount! - a.amount!); 138 | }); 139 | 140 | export const monthlyBudgetsSelector = createSelector([generalState], (expenses: any) => { 141 | return expenses.budgets; 142 | }); 143 | 144 | export default expensesReducer.reducer; 145 | -------------------------------------------------------------------------------- /components/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { Alert, TouchableOpacity } from "react-native"; 3 | import { VStack, Text, Pressable, Icon } from "native-base"; 4 | import { Feather } from "@expo/vector-icons"; 5 | import { useDispatch } from "react-redux"; 6 | import { setCurrency, setUser } from "../redux/userReducer"; 7 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 8 | import { UserService } from "../api/services/UserService"; 9 | import { Provider } from "../interfaces/Provider"; 10 | import { useFormik } from "formik"; 11 | import { loginSchema } from "../schemas/loginSchema"; 12 | import EZInput from "./shared/EZInput"; 13 | import COLORS from "../colors"; 14 | import EZButton from "./shared/EZButton"; 15 | import { authInput } from "../commonStyles"; 16 | 17 | interface LoginFormProps { 18 | navigation: NavigationProp; 19 | } 20 | 21 | const LoginForm: React.FC = ({ navigation }) => { 22 | const passwordRef = useRef(null); 23 | 24 | const [passwordVisilble, setPasswordVisible] = useState(false); 25 | const [loading, setLoading] = useState(false); 26 | const formik = useFormik({ 27 | initialValues: { 28 | email: "", 29 | password: "", 30 | }, 31 | validationSchema: loginSchema, 32 | onSubmit: async (values) => { 33 | const { email, password } = values; 34 | const response = await UserService.loginUser(email, password, Provider.DIRECT); 35 | const message: any = response.message; 36 | if (message === "User exists") { 37 | const { id, first_name, last_name, email, currency_code, currency_symbol } = response.data; 38 | 39 | dispatch(setUser({ firstName: first_name, lastName: last_name, email, id })); 40 | 41 | if (!currency_code || !currency_symbol) { 42 | navigation.navigate("Currency"); 43 | } else { 44 | dispatch(setCurrency({ name: currency_code, symbol: currency_symbol })); 45 | navigation.navigate("Tabs", { screen: "Home" }); 46 | } 47 | } else { 48 | Alert.alert("Error", message); 49 | } 50 | }, 51 | }); 52 | 53 | useEffect(() => { 54 | navigation.addListener("focus", () => { 55 | formik.resetForm(); 56 | setPasswordVisible(false); 57 | }); 58 | }, [navigation]); 59 | 60 | const dispatch = useDispatch(); 61 | 62 | const togglePasswordVisible = () => { 63 | setPasswordVisible((prevValue) => !prevValue); 64 | }; 65 | 66 | const login = async () => { 67 | setLoading(true); 68 | await submitForm(); 69 | setLoading(false); 70 | }; 71 | 72 | const handleValue = (label: string, value: string) => { 73 | formik.setFieldValue(label, value); 74 | }; 75 | 76 | const focusNextInput = (nextInputRef: any) => { 77 | nextInputRef.current.focus(); 78 | }; 79 | 80 | const goToForgotPassword = () => { 81 | navigation.navigate("ResetPassword"); 82 | }; 83 | 84 | const { values, errors, touched, submitForm } = formik; 85 | 86 | return ( 87 | 88 | 89 | 90 | Welcome to ExpenseZen 91 | 92 | 93 | Sign in to your account 94 | 95 | 96 | 97 | handleValue("email", e)} 107 | error={touched.email && errors.email} 108 | onSubmitEditing={() => { 109 | focusNextInput(passwordRef); 110 | }} 111 | /> 112 | handleValue("password", e)} 121 | borderRadius="12px" 122 | borderColor="muted.200" 123 | error={touched.password && errors.password} 124 | InputRightElement={ 125 | 126 | } 130 | /> 131 | 132 | } 133 | /> 134 | 135 | 136 | Forgot your password ? 137 | 138 | 139 | 140 | 141 | 150 | Sign in 151 | 152 | 153 | ); 154 | }; 155 | 156 | export default LoginForm; 157 | -------------------------------------------------------------------------------- /assets/SVG/SetBudget.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/SVG/Savings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screens/OnboardingScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState } from "react"; 2 | import { FlatList, Animated, useWindowDimensions } from "react-native"; 3 | import { Button, HStack, View, VStack } from "native-base"; 4 | import { PieChart, Progress, Savings } from "../assets/SVG"; 5 | import { Step } from "../interfaces/Step"; 6 | import OnboardingStep from "../components/OnboardingStep"; 7 | import Paginator from "../components/Paginator"; 8 | import { Ionicons } from "@expo/vector-icons"; 9 | import COLORS from "../colors"; 10 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 11 | import { useDispatch } from "react-redux"; 12 | import { onBoard } from "../redux/onboardReducer"; 13 | 14 | enum Direction { 15 | Back = "Back", 16 | Next = "Next", 17 | } 18 | 19 | const steps: Step[] = [ 20 | { 21 | id: 1, 22 | title: "Track Your Savings", 23 | description: 24 | "Start your financial journey by tracking your savings. Set goals, monitor your progress, and watch your savings grow over time", 25 | image: , 26 | }, 27 | { 28 | id: 2, 29 | title: "Visualize Your Expenses", 30 | description: 31 | "Gain insights into your spending habits with our interactive pie charts. See where your money is going and make informed decisions to manage your expenses effectively", 32 | image: , 33 | }, 34 | { 35 | id: 3, 36 | title: "Stay on Top of Your Financial Goals", 37 | description: 38 | "Keep track of your financial goals with our intuitive progress bars. Set milestones, track your achievements, and stay motivated on your path to financial success.", 39 | image: , 40 | }, 41 | ]; 42 | 43 | const bgs: string[] = [ 44 | COLORS.BLUE[400], 45 | COLORS.EMERALD[400], 46 | COLORS.YELLOW[400], 47 | ]; 48 | 49 | interface OnBoardingScreenProps { 50 | navigation: NavigationProp; 51 | } 52 | 53 | const OnboardingScreen: React.FC = ({ navigation }) => { 54 | const [currentIndex, setCurrentIndex] = useState(0); 55 | const { width, height } = useWindowDimensions(); 56 | const dispatch = useDispatch(); 57 | 58 | const scrollX = useRef(new Animated.Value(0)).current; 59 | const flatListRef = useRef>(null); 60 | 61 | const viewConfigRef = React.useRef({ 62 | viewAreaCoveragePercentThreshold: 50, 63 | }); 64 | 65 | const onChangeViewableItem = useCallback(({ viewableItems }: any) => { 66 | setCurrentIndex(viewableItems[0].index); 67 | }, []); 68 | 69 | const goDirection = async (direction: Direction) => { 70 | if (currentIndex === steps.length - 1 && direction === Direction.Next) { 71 | dispatch(onBoard()); 72 | navigation.navigate("Login"); 73 | } else { 74 | flatListRef.current?.scrollToIndex({ 75 | index: 76 | direction === Direction.Back ? currentIndex - 1 : currentIndex + 1, 77 | }); 78 | } 79 | }; 80 | 81 | const backgroundColor = scrollX.interpolate({ 82 | inputRange: bgs.map((_, i) => i * width), 83 | outputRange: bgs.map((bg) => bg), 84 | }); 85 | 86 | const isFirstStep = currentIndex === 0; 87 | 88 | return ( 89 | 97 | 98 | 99 | } 103 | pagingEnabled={true} 104 | bounces={false} 105 | horizontal={true} 106 | showsHorizontalScrollIndicator={false} 107 | data={steps} 108 | keyExtractor={(step: Step) => step.id.toString()} 109 | onScroll={Animated.event( 110 | [ 111 | { 112 | nativeEvent: { 113 | contentOffset: { 114 | x: scrollX, 115 | }, 116 | }, 117 | }, 118 | ], 119 | { useNativeDriver: false } 120 | )} 121 | scrollEventThrottle={1} 122 | onViewableItemsChanged={onChangeViewableItem} 123 | viewabilityConfig={viewConfigRef.current} 124 | /> 125 | 126 | 127 | 128 | 135 | 155 | 173 | 174 | 175 | ); 176 | }; 177 | 178 | export default OnboardingScreen; 179 | -------------------------------------------------------------------------------- /api/services/UserService.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../../interfaces/User"; 2 | import { supabase } from "../supabase"; 3 | import { compareHashed } from "../../utils/compareHashed"; 4 | import { hashPassword } from "../../utils/hashPassword"; 5 | import { Provider } from "../../interfaces/Provider"; 6 | import { Budget } from "../../interfaces/Budget"; 7 | import { MONTHLY_BUDGETS, USERS } from "../../constants/Tables"; 8 | import { CONVERT_BUDGETS_CURRENCY, GET_USER_BUDGETS } from "../../constants/PostgresFunctions"; 9 | 10 | const registerUser = async (user: User) => { 11 | const { firstName, lastName, email, password, provider } = user; 12 | 13 | try { 14 | const { data } = await supabase 15 | .from(USERS) 16 | .select("*") 17 | .filter("email", "eq", email) 18 | .filter("provider", "eq", provider) 19 | .single(); 20 | 21 | if (data) { 22 | return { 23 | title: "Try again", 24 | message: "This user is already registered !", 25 | }; 26 | } else { 27 | const { error } = await supabase.from(USERS).insert( 28 | [ 29 | { 30 | first_name: firstName, 31 | last_name: lastName, 32 | email, 33 | password: await hashPassword(password), 34 | provider, 35 | }, 36 | ], 37 | { 38 | returning: "minimal", 39 | } 40 | ); 41 | if (error) { 42 | throw error; 43 | } 44 | 45 | return { 46 | title: "Success", 47 | message: "User succesfully created !", 48 | }; 49 | } 50 | } catch (error) { 51 | if (error instanceof Error) { 52 | console.log(error); 53 | } 54 | } 55 | }; 56 | 57 | const resetPassword = async (email: string, newPassword: string) => { 58 | try { 59 | const { data } = await supabase.from(USERS).select("*").filter("email", "eq", email).single(); 60 | 61 | if (!data) { 62 | return { 63 | message: "This user does not exist !", 64 | }; 65 | } else { 66 | await supabase 67 | .from(USERS) 68 | .update({ password: await hashPassword(newPassword) }) 69 | .filter("email", "eq", email); 70 | 71 | return { 72 | message: "Password resetted succesfully !", 73 | }; 74 | } 75 | } catch (error) { 76 | if (error instanceof Error) { 77 | console.log(error); 78 | } 79 | } 80 | }; 81 | 82 | const changePassword = async (email: string, newPassword: string) => { 83 | const hashedPassword = await hashPassword(newPassword); 84 | 85 | try { 86 | const { error } = await supabase 87 | .from(USERS) 88 | .update({ password: hashedPassword }) 89 | .filter("email", "eq", email); 90 | 91 | if (error) { 92 | throw error; 93 | } 94 | 95 | return { 96 | message: "Password changed succesfully !", 97 | }; 98 | } catch (error) { 99 | if (error instanceof Error) { 100 | console.log(error); 101 | } 102 | } 103 | }; 104 | 105 | const loginUser = async (email: string, password: string, provider: Provider) => { 106 | try { 107 | const { data } = await supabase 108 | .from(USERS) 109 | .select("*") 110 | .filter("email", "eq", email) 111 | .filter("provider", "eq", provider) 112 | .single(); 113 | 114 | if (data && (await compareHashed(password, data.password))) { 115 | return { 116 | message: "User exists", 117 | data: data, 118 | }; 119 | } else { 120 | return { 121 | message: "Invalid email or password !", 122 | }; 123 | } 124 | } catch (error) { 125 | return { 126 | message: error, 127 | }; 128 | } 129 | }; 130 | 131 | const getUserBudgets = async (userId: number) => { 132 | try { 133 | const { data } = await supabase.rpc(GET_USER_BUDGETS, { 134 | user_id: userId, 135 | }); 136 | 137 | if (data) { 138 | return data; 139 | } 140 | return 0; 141 | } catch (error) { 142 | if (error instanceof Error) { 143 | console.log(error); 144 | } 145 | } 146 | }; 147 | 148 | const saveUserBudgets = async (userId: number, budgets: Budget[]) => { 149 | try { 150 | for (const item of budgets) { 151 | const { data } = await supabase 152 | .from(MONTHLY_BUDGETS) 153 | .select("*") 154 | .filter("category_id", "eq", item.id) 155 | .filter("user_id", "eq", userId) 156 | .single(); 157 | 158 | if (data) { 159 | await supabase 160 | .from(MONTHLY_BUDGETS) 161 | .update({ budget: item.budget }) 162 | .filter("category_id", "eq", item.id) 163 | .filter("user_id", "eq", userId); 164 | } else { 165 | await supabase.from(MONTHLY_BUDGETS).insert({ 166 | user_id: userId, 167 | category_id: item.id, 168 | budget: item.budget, 169 | }); 170 | } 171 | } 172 | } catch (error) { 173 | if (error instanceof Error) { 174 | console.log(error); 175 | } 176 | } 177 | }; 178 | 179 | const convertUserBudgetsCurrency = async (userId: number, conversionRate: number) => { 180 | try { 181 | const { error } = await supabase.rpc(CONVERT_BUDGETS_CURRENCY, { 182 | user_id: userId, 183 | conversion_rate: conversionRate, 184 | }); 185 | 186 | if (error) { 187 | throw error; 188 | } 189 | } catch (error) { 190 | if (error instanceof Error) { 191 | console.log(error); 192 | } 193 | } 194 | }; 195 | 196 | const removeUserBudgets = async (userId: number) => { 197 | try { 198 | const { error } = await supabase.from(MONTHLY_BUDGETS).delete().filter("user_id", "eq", userId); 199 | 200 | if (error) { 201 | throw error; 202 | } 203 | } catch (error) { 204 | if (error instanceof Error) { 205 | console.log(error); 206 | } 207 | } 208 | }; 209 | 210 | export const UserService = { 211 | registerUser, 212 | loginUser, 213 | getUserBudgets, 214 | saveUserBudgets, 215 | convertUserBudgetsCurrency, 216 | resetPassword, 217 | changePassword, 218 | removeUserBudgets, 219 | }; 220 | -------------------------------------------------------------------------------- /components/RegisterForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import { Alert } from "react-native"; 3 | import { VStack, Text } from "native-base"; 4 | import EZInput from "./shared/EZInput"; 5 | import { useFormik } from "formik"; 6 | import { registerSchema } from "../schemas/registerSchema"; 7 | import COLORS from "../colors"; 8 | import { UserService } from "../api/services/UserService"; 9 | import { User } from "../interfaces/User"; 10 | import { Provider } from "../interfaces/Provider"; 11 | import { useNavigation } from "@react-navigation/native"; 12 | import { NativeStackNavigationProp } from "@react-navigation/native-stack"; 13 | import EZButton from "./shared/EZButton"; 14 | import { AppStackParamList } from "../interfaces/Navigation"; 15 | import { authInput } from "../commonStyles"; 16 | 17 | const RegisterForm: React.FC = () => { 18 | const [loading, setLoading] = useState(false); 19 | 20 | const lastNameRef = useRef(null); 21 | const emailRef = useRef(null); 22 | const passwordRef = useRef(null); 23 | const repeatPasswordRef = useRef(null); 24 | 25 | const navigation = useNavigation>(); 26 | 27 | const formik = useFormik({ 28 | initialValues: { 29 | firstName: "", 30 | lastName: "", 31 | email: "", 32 | password: "", 33 | repeatPassword: "", 34 | }, 35 | validationSchema: registerSchema, 36 | onSubmit: async (values) => { 37 | const user: User = { 38 | firstName: values.firstName, 39 | lastName: values.lastName, 40 | email: values.email, 41 | 42 | password: values.password, 43 | provider: Provider.DIRECT, 44 | }; 45 | 46 | try { 47 | const response = await UserService.registerUser(user); 48 | 49 | Alert.alert(response!.title, response!.message); 50 | 51 | if (response!.title === "Try again") { 52 | formik.resetForm(); 53 | } else { 54 | navigation.navigate("Login"); 55 | } 56 | } catch (error) { 57 | console.log(error); 58 | } 59 | }, 60 | }); 61 | 62 | const { values, errors, submitForm, touched } = formik; 63 | 64 | const handleValue = (label: string, value: string) => { 65 | formik.setFieldValue(label, value); 66 | }; 67 | 68 | const focusNextInput = (nextInputRef: any) => { 69 | nextInputRef.current.focus(); 70 | }; 71 | 72 | const submit = async () => { 73 | setLoading(true); 74 | await submitForm(); 75 | setLoading(false); 76 | }; 77 | 78 | return ( 79 | 80 | 81 | 82 | Register account 83 | 84 | 85 | Create an account to continue 86 | 87 | 88 | 89 | handleValue("firstName", e)} 99 | error={touched.firstName && errors.firstName} 100 | onSubmitEditing={() => { 101 | focusNextInput(lastNameRef); 102 | }} 103 | /> 104 | handleValue("lastName", e)} 115 | error={touched.lastName && errors.lastName} 116 | onSubmitEditing={() => { 117 | focusNextInput(emailRef); 118 | }} 119 | /> 120 | handleValue("email", e)} 131 | error={touched.email && errors.email} 132 | onSubmitEditing={() => { 133 | focusNextInput(passwordRef); 134 | }} 135 | /> 136 | handleValue("password", e)} 148 | error={touched.password && errors.password} 149 | onSubmitEditing={() => { 150 | focusNextInput(repeatPasswordRef); 151 | }} 152 | /> 153 | handleValue("repeatPassword", e)} 165 | error={touched.repeatPassword && errors.repeatPassword} 166 | /> 167 | 168 | 169 | 178 | Register 179 | 180 | 181 | ); 182 | }; 183 | 184 | export default RegisterForm; 185 | -------------------------------------------------------------------------------- /screens/GraphScreen.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 2 | import { 3 | View, 4 | Text, 5 | Box, 6 | VStack, 7 | Circle, 8 | HStack, 9 | Skeleton, 10 | ScrollView, 11 | FlatList, 12 | useTheme, 13 | } from "native-base"; 14 | import React, { useLayoutEffect, useState, Fragment } from "react"; 15 | import PieChart from "react-native-pie-chart"; 16 | import { useSelector } from "react-redux"; 17 | import { Expense } from "../interfaces/Expense"; 18 | import { RootState } from "../redux/store"; 19 | import { GraphCategory } from "../interfaces/GraphCategory"; 20 | import GraphCategoryItem from "../components/GraphCategoryItem"; 21 | import { StatusBar } from "expo-status-bar"; 22 | import { monthTotalSelector } from "../redux/expensesReducers"; 23 | import EZHeaderTitle from "../components/shared/EzHeaderTitle"; 24 | import { NoChartData } from "../assets/SVG"; 25 | 26 | interface GraphScreenProps { 27 | navigation: NavigationProp; 28 | } 29 | 30 | const GraphScreen: React.FC = ({ navigation }) => { 31 | const user = useSelector((state: RootState) => state.user); 32 | const [graphCategories, setGraphCategories] = useState([]); 33 | const [loading, setLoading] = useState(false); 34 | const [series, setSeries] = useState([1]); 35 | const [colors, setColors] = useState(["red"]); 36 | const { expenses } = useSelector((state: RootState) => state.expenses); 37 | const monthTotal = useSelector(monthTotalSelector); 38 | const { 39 | colors: { muted }, 40 | } = useTheme(); 41 | 42 | useLayoutEffect(() => { 43 | navigation.setOptions({ 44 | headerTitle: () => Graph Reports, 45 | }); 46 | 47 | getGeneralInfo(); 48 | }, [navigation]); 49 | 50 | const getGeneralInfo = async () => { 51 | setLoading(true); 52 | 53 | const categorySummary = expenses!.reduce((acc: any, current: any) => { 54 | const { name, amount, color } = current; 55 | if (acc.hasOwnProperty(name)) { 56 | acc[name].amount += amount; 57 | acc[name].expenses += 1; 58 | acc[name].color = color; 59 | } else { 60 | acc[name] = { amount, expenses: 1, color }; 61 | } 62 | return acc; 63 | }, {}); 64 | 65 | if (expenses && expenses.length > 0) { 66 | const graphCategories = Object.keys(categorySummary).map((category) => ({ 67 | name: category, 68 | amount: categorySummary[category].amount, 69 | expenses: categorySummary[category].expenses, 70 | color: categorySummary[category].color, 71 | })); 72 | 73 | setGraphCategories(graphCategories); 74 | 75 | setSeries(graphCategories.map((item: Expense) => item.amount)); 76 | setColors(graphCategories.map((item: GraphCategory) => item.color)); 77 | } 78 | 79 | setLoading(false); 80 | }; 81 | 82 | return ( 83 | 84 | 85 | 0 ? "space-between" : "center"} 88 | w="90%" 89 | py={4} 90 | px={2} 91 | style={{ 92 | shadowColor: "#171717", 93 | shadowOffset: { width: 0, height: 0 }, 94 | shadowOpacity: 0.1, 95 | shadowRadius: 4, 96 | }} 97 | bg="muted.50" 98 | borderRadius={20}> 99 | {expenses && expenses.length > 0 ? ( 100 | 101 | 102 | {loading ? ( 103 | 104 | ) : ( 105 | 112 | )} 113 | 114 | {!loading && ( 115 | 116 | 117 | Total 118 | 119 | 120 | {user.symbol} {monthTotal.toFixed(2)} 121 | 122 | 123 | )} 124 | 125 | 126 | 127 | 128 | 129 | {graphCategories.map((graphCategory: GraphCategory, key: number) => { 130 | const categoryPercent = (graphCategory.amount * 100) / monthTotal; 131 | return ( 132 | 133 | 134 | 135 | {graphCategory.name} ({categoryPercent.toFixed(2)}%) 136 | 137 | 138 | ); 139 | })} 140 | 141 | 142 | 143 | 144 | ) : ( 145 | 146 | 147 | 148 | No data to display 149 | 150 | 151 | )} 152 | 153 | 154 | item.name} 161 | ItemSeparatorComponent={() => } 162 | renderItem={({ item }) => ( 163 | expense.name === item.name)} 165 | graphCategory={item} 166 | /> 167 | )} 168 | /> 169 | 170 | ); 171 | }; 172 | export default GraphScreen; 173 | -------------------------------------------------------------------------------- /assets/SVG/Progress.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screens/AddExpenseScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import { HStack, Text, VStack, View, useTheme } from "native-base"; 3 | import { SafeAreaView, FlatList, useWindowDimensions, TouchableOpacity } from "react-native"; 4 | import { AntDesign, FontAwesome } from "@expo/vector-icons"; 5 | import { NavigationProp, ParamListBase } from "@react-navigation/native"; 6 | import EZInput from "../components/shared/EZInput"; 7 | import { Category } from "../interfaces/Category"; 8 | import CategoryItem from "../components/CategoryItem"; 9 | import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; 10 | import { useFormik } from "formik"; 11 | import { expenseSchema } from "../schemas/expenseSchema"; 12 | import EZButton from "../components/shared/EZButton"; 13 | import COLORS from "../colors"; 14 | import { ExpenseService } from "../api/services/ExpenseService"; 15 | import { useDispatch, useSelector } from "react-redux"; 16 | import { RootState } from "../redux/store"; 17 | import { StatusBar } from "expo-status-bar"; 18 | import { addExpenseAction, categoriesSelector } from "../redux/expensesReducers"; 19 | import moment from "moment"; 20 | import { authInput } from "../commonStyles"; 21 | 22 | interface AddExpenseScreenProps { 23 | navigation: NavigationProp; 24 | } 25 | 26 | const AddExpenseScreen: React.FC = ({ navigation }) => { 27 | const { width } = useWindowDimensions(); 28 | const dispatch = useDispatch(); 29 | const scrollRef = useRef(null); 30 | const { 31 | colors: { muted }, 32 | } = useTheme(); 33 | 34 | const user = useSelector((state: RootState) => state.user); 35 | const categories = useSelector(categoriesSelector); 36 | const [loading, setLoading] = useState(false); 37 | const formik = useFormik({ 38 | initialValues: { 39 | amount: "", 40 | category: "", 41 | description: "", 42 | }, 43 | validationSchema: expenseSchema, 44 | onSubmit: async (values) => { 45 | const { amount, category, description } = values; 46 | 47 | try { 48 | const currentCategory = categories.find((item: Category) => item.name === category); 49 | 50 | const formatAmount = amount.replace(",", "."); 51 | const numericFormat = Number(formatAmount); 52 | 53 | const expense = { 54 | userId: Number(user.id), 55 | categoryId: Number(currentCategory!.id), 56 | amount: numericFormat, 57 | description, 58 | }; 59 | 60 | const today = moment().format("YYYY-MM-DD"); 61 | await ExpenseService.AddExpense(expense); 62 | 63 | dispatch( 64 | addExpenseAction({ 65 | ...expense, 66 | payDate: today, 67 | name: category, 68 | color: currentCategory.color, 69 | }) 70 | ); 71 | 72 | navigation.goBack(); 73 | } catch (error) { 74 | console.log(error); 75 | } 76 | }, 77 | }); 78 | 79 | const handleValue = (label: string, value: string) => { 80 | if (label === "amount" && values.amount.includes(",") && value.slice(-1) === ",") { 81 | formik.setFieldValue(label, value.slice(0, -1)); 82 | } else { 83 | formik.setFieldValue(label, value); 84 | } 85 | }; 86 | 87 | const selectCategory = (value: string) => { 88 | formik.setFieldValue("category", value); 89 | }; 90 | 91 | const { values, errors, submitForm, touched } = formik; 92 | 93 | const addExpense = async () => { 94 | setLoading(true); 95 | await submitForm(); 96 | setLoading(false); 97 | }; 98 | 99 | return ( 100 | 101 | 102 | scrollRef.current.scrollToEnd()}> 106 | navigation.goBack()}> 109 | 110 | 111 | 112 | 113 | 114 | 115 | Add new expense 116 | 117 | handleValue("amount", e)} 124 | label={`Enter amount ${user.symbol}`} 125 | borderRadius={12} 126 | borderColor="muted.100" 127 | placeholderTextColor="muted.300" 128 | _focus={{ 129 | backgroundColor: "transparent", 130 | color: "purple.700", 131 | placeholderTextColor: "purple.700", 132 | }} 133 | error={touched.amount && errors.amount} 134 | /> 135 | 136 | Category 137 | 138 | } 147 | data={categories} 148 | renderItem={({ item }) => ( 149 | 155 | )} 156 | /> 157 | 158 | {touched.category && errors.category && ( 159 | 160 | 161 | 162 | {errors.category} 163 | 164 | 165 | )} 166 | handleValue("description", e)} 173 | borderRadius={12} 174 | borderColor="muted.200" 175 | error={touched.description && errors.description} 176 | /> 177 | 178 | 191 | SAVE 192 | 193 | 194 | 195 | 196 | 197 | ); 198 | }; 199 | 200 | export default AddExpenseScreen; 201 | --------------------------------------------------------------------------------