├── .gitignore
├── app.json
├── assets
├── adaptive-icon.png
├── favicon.png
├── icon.png
└── splash.png
├── babel.config.js
├── package-lock.json
├── package.json
├── src
├── app
│ ├── (auth)
│ │ ├── _layout.tsx
│ │ └── login.tsx
│ ├── (tabs)
│ │ ├── _layout.tsx
│ │ ├── accounts.tsx
│ │ └── allocations
│ │ │ ├── _layout.tsx
│ │ │ ├── index.tsx
│ │ │ └── new.tsx
│ ├── _layout.tsx
│ └── index.tsx
├── components
│ ├── AccountAllocationItem.tsx
│ ├── AccountListItem.tsx
│ ├── AccountsList.tsx
│ ├── AllocationListItem.tsx
│ └── AllocationsList.tsx
├── db
│ ├── index.ts
│ ├── migrations.ts
│ ├── schema.ts
│ └── sync.ts
├── lib
│ └── supabase.ts
├── model
│ ├── Account.ts
│ ├── AccountAllocation.ts
│ └── Allocation.ts
└── providers
│ └── AuthProvider.tsx
├── supabase
├── .gitignore
├── config.toml
├── migrations
│ └── 20240426181512_remote_schema.sql
└── seed.sql
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | ios
38 | android
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "ProfitFirst",
4 | "slug": "ProfitFirst",
5 | "scheme": "profitfirst",
6 | "version": "1.0.0",
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.vadinsavin.ProfitFirst"
19 | },
20 | "android": {
21 | "adaptiveIcon": {
22 | "foregroundImage": "./assets/adaptive-icon.png",
23 | "backgroundColor": "#ffffff"
24 | },
25 | "package": "com.vadinsavin.ProfitFirst"
26 | },
27 | "web": {
28 | "favicon": "./assets/favicon.png",
29 | "bundler": "metro"
30 | },
31 | "plugins": [
32 | "expo-router",
33 | [
34 | "expo-build-properties",
35 | {
36 | "ios": {
37 | "extraPods": [
38 | {
39 | "name": "simdjson",
40 | "configurations": ["Debug", "Release"],
41 | "path": "../node_modules/@nozbe/simdjson",
42 | "modular_headers": true
43 | }
44 | ]
45 | }
46 | // "android": {
47 | // "kotlinVersion": ''
48 | // }
49 | }
50 | ]
51 | ]
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notJust-dev/ProfitFirst/7e4b569c396a7a06af5bdef0d16165296cf5ad52/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notJust-dev/ProfitFirst/7e4b569c396a7a06af5bdef0d16165296cf5ad52/assets/favicon.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notJust-dev/ProfitFirst/7e4b569c396a7a06af5bdef0d16165296cf5ad52/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notJust-dev/ProfitFirst/7e4b569c396a7a06af5bdef0d16165296cf5ad52/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: [['@babel/plugin-proposal-decorators', { legacy: true }]],
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "profitfirst",
3 | "version": "1.0.0",
4 | "main": "expo-router/entry",
5 | "scripts": {
6 | "start": "expo start --dev-client",
7 | "android": "expo run:android",
8 | "ios": "expo run:ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@nozbe/watermelondb": "^0.27.1",
13 | "@react-native-async-storage/async-storage": "1.21.0",
14 | "@supabase/supabase-js": "^2.42.7",
15 | "@types/react": "~18.2.45",
16 | "expo": "~50.0.14",
17 | "expo-build-properties": "~0.11.1",
18 | "expo-constants": "~15.4.6",
19 | "expo-linking": "~6.2.2",
20 | "expo-router": "~3.4.8",
21 | "expo-status-bar": "~1.11.1",
22 | "react": "18.2.0",
23 | "react-dom": "18.2.0",
24 | "react-native": "0.73.6",
25 | "react-native-safe-area-context": "4.8.2",
26 | "react-native-screens": "~3.29.0",
27 | "react-native-web": "~0.19.6",
28 | "typescript": "^5.3.0",
29 | "expo-crypto": "~12.8.1"
30 | },
31 | "devDependencies": {
32 | "@babel/core": "^7.20.0",
33 | "@babel/plugin-proposal-decorators": "^7.24.1"
34 | },
35 | "private": true
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/(auth)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Redirect, Stack } from 'expo-router';
2 | import { useAuth } from '../../providers/AuthProvider';
3 |
4 | const AuthLayout = () => {
5 | const { isAuthenticated } = useAuth();
6 |
7 | if (isAuthenticated) {
8 | return ;
9 | }
10 |
11 | return ;
12 | };
13 |
14 | export default AuthLayout;
15 |
--------------------------------------------------------------------------------
/src/app/(auth)/login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Alert,
4 | StyleSheet,
5 | View,
6 | AppState,
7 | Button,
8 | TextInput,
9 | } from 'react-native';
10 | import { supabase } from '../../lib/supabase';
11 |
12 | // Tells Supabase Auth to continuously refresh the session automatically if
13 | // the app is in the foreground. When this is added, you will continue to receive
14 | // `onAuthStateChange` events with the `TOKEN_REFRESHED` or `SIGNED_OUT` event
15 | // if the user's session is terminated. This should only be registered once.
16 | AppState.addEventListener('change', (state) => {
17 | if (state === 'active') {
18 | supabase.auth.startAutoRefresh();
19 | } else {
20 | supabase.auth.stopAutoRefresh();
21 | }
22 | });
23 |
24 | export default function Auth() {
25 | const [email, setEmail] = useState('');
26 | const [password, setPassword] = useState('');
27 | const [loading, setLoading] = useState(false);
28 |
29 | async function signInWithEmail() {
30 | setLoading(true);
31 | const { error } = await supabase.auth.signInWithPassword({
32 | email: email,
33 | password: password,
34 | });
35 |
36 | if (error) Alert.alert(error.message);
37 | setLoading(false);
38 | }
39 |
40 | async function signUpWithEmail() {
41 | setLoading(true);
42 | const {
43 | data: { session },
44 | error,
45 | } = await supabase.auth.signUp({
46 | email: email,
47 | password: password,
48 | });
49 |
50 | if (error) Alert.alert(error.message);
51 | if (!session)
52 | Alert.alert('Please check your inbox for email verification!');
53 | setLoading(false);
54 | }
55 |
56 | return (
57 |
58 |
59 | setEmail(text)}
61 | value={email}
62 | placeholder="email@address.com"
63 | autoCapitalize={'none'}
64 | style={styles.input}
65 | />
66 |
67 |
68 | setPassword(text)}
70 | value={password}
71 | secureTextEntry={true}
72 | placeholder="Password"
73 | autoCapitalize={'none'}
74 | style={styles.input}
75 | />
76 |
77 |
78 |
79 |
85 |
86 |
92 |
93 | );
94 | }
95 |
96 | const styles = StyleSheet.create({
97 | container: {
98 | marginTop: 40,
99 | padding: 12,
100 | },
101 | verticallySpaced: {
102 | paddingTop: 4,
103 | paddingBottom: 4,
104 | alignSelf: 'stretch',
105 | },
106 | mt20: {
107 | marginTop: 20,
108 | },
109 | input: {
110 | backgroundColor: 'white',
111 | padding: 15,
112 | borderRadius: 5,
113 | },
114 | });
115 |
--------------------------------------------------------------------------------
/src/app/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Redirect, Tabs } from 'expo-router';
2 | import { MaterialIcons } from '@expo/vector-icons';
3 | import { useAuth } from '../../providers/AuthProvider';
4 |
5 | export default function TabsLayout() {
6 | const { isAuthenticated } = useAuth();
7 |
8 | if (!isAuthenticated) {
9 | return ;
10 | }
11 |
12 | return (
13 |
14 | (
20 |
21 | ),
22 | }}
23 | />
24 | (
29 |
34 | ),
35 | }}
36 | />
37 | {/* */}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/(tabs)/accounts.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet, Button, TextInput } from 'react-native';
2 | import AccountsList from '../../components/AccountsList';
3 | import { useState } from 'react';
4 | import database, { accountsCollection } from '../../db';
5 | import { useAuth } from '../../providers/AuthProvider';
6 |
7 | export default function AccountsScreen() {
8 | const [name, setName] = useState('');
9 | const [cap, setCap] = useState('');
10 | const [tap, setTap] = useState('');
11 |
12 | const { user } = useAuth();
13 |
14 | const createAccount = async () => {
15 | await database.write(async () => {
16 | await accountsCollection.create((account) => {
17 | account.name = name;
18 | account.cap = Number.parseFloat(cap);
19 | account.tap = Number.parseFloat(tap);
20 | account.userId = user?.id;
21 | });
22 | });
23 | setName('');
24 | setCap('');
25 | setTap('');
26 | };
27 |
28 | return (
29 |
30 |
31 | Name
32 | CAP
33 | TAP
34 |
35 |
36 |
37 |
38 |
39 |
45 |
51 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | const styles = StyleSheet.create({
65 | header: {
66 | flexDirection: 'row',
67 | justifyContent: 'space-between',
68 | padding: 10,
69 | },
70 | inputRow: {
71 | flexDirection: 'row',
72 | justifyContent: 'space-between',
73 | padding: 10,
74 | backgroundColor: 'white',
75 | },
76 | input: {
77 | flex: 1,
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/src/app/(tabs)/allocations/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Stack } from 'expo-router';
2 |
3 | export default function AllocationsLayout() {
4 | return (
5 |
6 | )
7 | }
--------------------------------------------------------------------------------
/src/app/(tabs)/allocations/index.tsx:
--------------------------------------------------------------------------------
1 | import { StatusBar } from 'expo-status-bar';
2 | import { Button, StyleSheet, Text, View } from 'react-native';
3 | import { Link, Stack } from 'expo-router';
4 | import AllocationsList from '../../../components/AllocationsList';
5 | import { Feather } from '@expo/vector-icons';
6 | import { mySync } from '../../../db/sync';
7 | import { supabase } from '../../../lib/supabase';
8 | import * as Crypto from 'expo-crypto';
9 |
10 | export default function HomeScreen() {
11 | const test = async () => {
12 | const res = await supabase.rpc('create_account', {
13 | _id: Crypto.randomUUID(),
14 | _user_id: Crypto.randomUUID(), // Replace 'uuid-string-here' with a valid UUID
15 | _name: 'Example Name', // Replace 'Example Name' with the actual account name
16 | _cap: 1000.0, // Set a numeric value for the cap
17 | _tap: 500.0, // Set a numeric value for the tap
18 | _created_at: new Date().toISOString(), // Current date-time in ISO string format
19 | _updated_at: new Date().toISOString(), // Current date-time in ISO string format
20 | });
21 | console.log(res);
22 | };
23 |
24 | return (
25 |
26 | (
30 |
36 | ),
37 | }}
38 | />
39 |
40 |
41 |
42 | New Allocation
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | const styles = StyleSheet.create({
53 | container: {
54 | flex: 1,
55 | },
56 | button: {
57 | backgroundColor: 'green',
58 | color: 'white',
59 | margin: 10,
60 | padding: 10,
61 | textAlign: 'center',
62 | fontWeight: 'bold',
63 | borderRadius: 5,
64 | overflow: 'hidden',
65 | },
66 | });
67 |
--------------------------------------------------------------------------------
/src/app/(tabs)/allocations/new.tsx:
--------------------------------------------------------------------------------
1 | import { Stack, router } from 'expo-router';
2 | import { useState } from 'react';
3 | import { View, Text, StyleSheet, TextInput, Button } from 'react-native';
4 | import database, {
5 | accountAllocationCollection,
6 | accountsCollection,
7 | allocationsCollection,
8 | } from '../../../db';
9 | import { withObservables } from '@nozbe/watermelondb/react';
10 | import Account from '../../../model/Account';
11 | import { useAuth } from '../../../providers/AuthProvider';
12 |
13 | function NewAllocationScreen({ accounts }: { accounts: Account[] }) {
14 | const [income, setIncome] = useState('0');
15 |
16 | const { user } = useAuth();
17 |
18 | const save = async () => {
19 | await database.write(async () => {
20 | const allocation = await allocationsCollection.create((newAllocation) => {
21 | newAllocation.income = Number.parseFloat(income);
22 | newAllocation.userId = user?.id;
23 | });
24 |
25 | await Promise.all(
26 | accounts.map((account) =>
27 | accountAllocationCollection.create((item) => {
28 | item.account.set(account);
29 | item.allocation.set(allocation);
30 | item.cap = account.cap;
31 | item.amount = (allocation.income * account.cap) / 100;
32 | item.userId = user?.id;
33 | })
34 | )
35 | );
36 | });
37 | setIncome('');
38 | router.back();
39 | };
40 |
41 | return (
42 |
43 |
44 |
45 |
46 | Income
47 |
53 |
54 |
55 | {accounts.map((account) => (
56 |
57 |
58 | {account.name}: {account.cap}%
59 |
60 | ${(Number.parseFloat(income) * account.cap) / 100}
61 |
62 | ))}
63 |
64 |
65 |
66 | );
67 | }
68 |
69 | const enhance = withObservables([], () => ({
70 | accounts: accountsCollection.query(),
71 | }));
72 |
73 | export default enhance(NewAllocationScreen);
74 |
75 | const styles = StyleSheet.create({
76 | container: {
77 | padding: 10,
78 | gap: 10,
79 | },
80 | label: {
81 | fontWeight: 'bold',
82 | width: 100,
83 | },
84 | inputRow: {
85 | flexDirection: 'row',
86 | alignItems: 'center',
87 | gap: 10,
88 | },
89 | input: {
90 | backgroundColor: 'white',
91 | padding: 10,
92 | borderRadius: 5,
93 | flex: 1,
94 | },
95 | });
96 |
--------------------------------------------------------------------------------
/src/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Redirect, Slot } from 'expo-router';
2 | import AuthProvider, { useAuth } from '../providers/AuthProvider';
3 |
4 | const RootLayout = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | export default RootLayout;
13 |
--------------------------------------------------------------------------------
/src/app/index.tsx:
--------------------------------------------------------------------------------
1 | import { Redirect } from 'expo-router';
2 | import { View, Text } from 'react-native';
3 |
4 | const HomeScreen = () => {
5 | return ;
6 | };
7 |
8 | export default HomeScreen;
9 |
--------------------------------------------------------------------------------
/src/components/AccountAllocationItem.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from 'react-native';
2 | import AccountAllocation from '../model/AccountAllocation';
3 | import { withObservables } from '@nozbe/watermelondb/react';
4 | import Account from '../model/Account';
5 |
6 | type AccountAllocationItem = {
7 | accountAllocation: AccountAllocation;
8 | account: Account;
9 | };
10 |
11 | const AccountAllocationItem = ({
12 | accountAllocation,
13 | account,
14 | }: AccountAllocationItem) => {
15 | return (
16 |
17 | {account.name}
18 | ${accountAllocation.amount}
19 |
20 | );
21 | };
22 |
23 | const enhance = withObservables(
24 | ['accountAllocation'],
25 | ({ accountAllocation }: { accountAllocation: AccountAllocation }) => ({
26 | accountAllocation,
27 | account: accountAllocation.account,
28 | })
29 | );
30 |
31 | export default enhance(AccountAllocationItem);
32 |
--------------------------------------------------------------------------------
/src/components/AccountListItem.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from 'react-native';
2 | import Account from '../model/Account';
3 | import { withObservables } from '@nozbe/watermelondb/react';
4 | import { AntDesign } from '@expo/vector-icons';
5 | import database from '../db';
6 |
7 | type AccountListItem = {
8 | account: Account;
9 | };
10 |
11 | function AccountListItem({ account }: AccountListItem) {
12 | const onDelete = async () => {
13 | await database.write(async () => {
14 | await account.markAsDeleted();
15 | });
16 | };
17 |
18 | return (
19 |
20 | {account.name}
21 | {account.cap}%
22 | {account.tap}%
23 |
24 |
25 | );
26 | }
27 |
28 | const enhance = withObservables(
29 | ['account'],
30 | ({ account }: AccountListItem) => ({
31 | account,
32 | })
33 | );
34 |
35 | export default enhance(AccountListItem);
36 |
37 | const styles = StyleSheet.create({
38 | container: {
39 | backgroundColor: 'white',
40 | padding: 10,
41 | flexDirection: 'row',
42 | justifyContent: 'space-between',
43 | borderRadius: 5,
44 | },
45 | name: {
46 | fontWeight: 'bold',
47 | fontSize: 16,
48 | flex: 1,
49 | },
50 | percentage: {
51 | flex: 1,
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/src/components/AccountsList.tsx:
--------------------------------------------------------------------------------
1 | import { FlatList } from 'react-native';
2 | import AccountListItem from './AccountListItem';
3 | import { accountsCollection } from '../db';
4 | import Account from '../model/Account';
5 |
6 | import { withObservables } from '@nozbe/watermelondb/react';
7 |
8 | function AccountsList({ accounts }: { accounts: Account[] }) {
9 | return (
10 | }
14 | />
15 | );
16 | }
17 |
18 | const enhance = withObservables([], () => ({
19 | accounts: accountsCollection.query(),
20 | }));
21 |
22 | export default enhance(AccountsList);
23 |
--------------------------------------------------------------------------------
/src/components/AllocationListItem.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from 'react-native';
2 | import Allocation from '../model/Allocation';
3 | import { withObservables } from '@nozbe/watermelondb/react';
4 | import AccountAllocation from '../model/AccountAllocation';
5 | import AccountAllocationItem from './AccountAllocationItem';
6 |
7 | type AllocationListItem = {
8 | allocation: Allocation;
9 | accountAllocations: AccountAllocation[];
10 | };
11 |
12 | const AllocationListItem = ({
13 | allocation,
14 | accountAllocations,
15 | }: AllocationListItem) => {
16 | return (
17 |
18 |
19 |
20 | {allocation.createdAt.toLocaleDateString()}
21 |
22 | ${allocation.income}
23 |
24 |
25 |
26 | {accountAllocations.map((item) => (
27 |
28 | ))}
29 |
30 |
31 | );
32 | };
33 |
34 | const enhance = withObservables(
35 | ['allocation'],
36 | ({ allocation }: { allocation: Allocation }) => ({
37 | allocation,
38 | accountAllocations: allocation.accountAllocations,
39 | })
40 | );
41 |
42 | export default enhance(AllocationListItem);
43 |
44 | const styles = StyleSheet.create({
45 | container: {
46 | borderRadius: 10,
47 | overflow: 'hidden',
48 | },
49 | header: {
50 | flexDirection: 'row',
51 | justifyContent: 'space-between',
52 | backgroundColor: 'gainsboro',
53 | padding: 10,
54 | },
55 | income: {
56 | fontSize: 24,
57 | fontWeight: 'bold',
58 | color: 'green',
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/src/components/AllocationsList.tsx:
--------------------------------------------------------------------------------
1 | import { FlatList } from 'react-native';
2 | import AllocationListItem from './AllocationListItem';
3 | import { withObservables } from '@nozbe/watermelondb/react';
4 | import { allocationsCollection } from '../db';
5 | import Allocation from '../model/Allocation';
6 | import { Q } from '@nozbe/watermelondb';
7 |
8 | function AllocationsList({ allocations }: { allocations: Allocation[] }) {
9 | return (
10 | }
14 | />
15 | );
16 | }
17 |
18 | const enhance = withObservables([], () => ({
19 | allocations: allocationsCollection.query(Q.sortBy('created_at', Q.desc)),
20 | }));
21 |
22 | export default enhance(AllocationsList);
23 |
--------------------------------------------------------------------------------
/src/db/index.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from 'react-native';
2 | import { Database } from '@nozbe/watermelondb';
3 | import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';
4 |
5 | import schema from './schema';
6 | import migrations from './migrations';
7 | import Account from '../model/Account';
8 | import Allocation from '../model/Allocation';
9 | import AccountAllocation from '../model/AccountAllocation';
10 | import * as Crypto from 'expo-crypto';
11 | import { setGenerator } from '@nozbe/watermelondb/utils/common/randomId';
12 | setGenerator(() => Crypto.randomUUID());
13 |
14 | // First, create the adapter to the underlying database:
15 | const adapter = new SQLiteAdapter({
16 | schema,
17 | // (You might want to comment it out for development purposes -- see Migrations documentation)
18 | // migrations,
19 | // (optional database name or file system path)
20 | // dbName: 'myapp',
21 | // (recommended option, should work flawlessly out of the box on iOS. On Android,
22 | // additional installation steps have to be taken - disable if you run into issues...)
23 | jsi: true /* Platform.OS === 'ios' */,
24 | // (optional, but you should implement this method)
25 | onSetUpError: (error) => {
26 | // Database failed to load -- offer the user to reload the app or log out
27 | },
28 | });
29 |
30 | // Then, make a Watermelon database from it!
31 | const database = new Database({
32 | adapter,
33 | modelClasses: [Account, Allocation, AccountAllocation],
34 | });
35 |
36 | export default database;
37 |
38 | export const accountsCollection = database.get('accounts');
39 | export const allocationsCollection = database.get('allocations');
40 | export const accountAllocationCollection = database.get(
41 | 'account_allocations'
42 | );
43 |
--------------------------------------------------------------------------------
/src/db/migrations.ts:
--------------------------------------------------------------------------------
1 | import { schemaMigrations } from '@nozbe/watermelondb/Schema/migrations';
2 |
3 | export default schemaMigrations({
4 | migrations: [
5 | // We'll add migration definitions here later
6 | ],
7 | });
8 |
--------------------------------------------------------------------------------
/src/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { appSchema, tableSchema } from '@nozbe/watermelondb';
2 |
3 | export default appSchema({
4 | version: 6,
5 | tables: [
6 | tableSchema({
7 | name: 'accounts',
8 | columns: [
9 | { name: 'created_at', type: 'number' },
10 | { name: 'updated_at', type: 'number' },
11 | { name: 'name', type: 'string' },
12 | { name: 'cap', type: 'number' },
13 | { name: 'tap', type: 'number' },
14 | { name: 'user_id', type: 'string' },
15 | ],
16 | }),
17 | tableSchema({
18 | name: 'allocations',
19 | columns: [
20 | { name: 'created_at', type: 'number' },
21 | { name: 'updated_at', type: 'number' },
22 | { name: 'income', type: 'number' },
23 | { name: 'user_id', type: 'string' },
24 | ],
25 | }),
26 | tableSchema({
27 | name: 'account_allocations',
28 | columns: [
29 | { name: 'created_at', type: 'number' },
30 | { name: 'updated_at', type: 'number' },
31 | { name: 'account_id', type: 'string' },
32 | { name: 'allocation_id', type: 'string' },
33 | { name: 'amount', type: 'number' },
34 | { name: 'cap', type: 'number' },
35 | { name: 'user_id', type: 'string' },
36 | ],
37 | }),
38 | ],
39 | });
40 |
--------------------------------------------------------------------------------
/src/db/sync.ts:
--------------------------------------------------------------------------------
1 | import { synchronize } from '@nozbe/watermelondb/sync';
2 | import database from './index';
3 | import { supabase } from '../lib/supabase';
4 |
5 | export async function mySync() {
6 | await synchronize({
7 | database,
8 | sendCreatedAsUpdated: true,
9 | pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
10 | console.log('Pulling data');
11 | const { data, error } = await supabase.rpc('pull', {
12 | last_pulled_at: lastPulledAt,
13 | schemaversion: schemaVersion,
14 | migration: migration,
15 | });
16 | console.log(error);
17 | console.log(JSON.stringify(data));
18 | return {
19 | changes: data.changes,
20 | timestamp: data.timestamp,
21 | };
22 | },
23 | pushChanges: async ({ changes, lastPulledAt }) => {
24 | console.log('Pushing data');
25 |
26 | const { error } = await supabase.rpc('push', { changes });
27 |
28 | console.log('Error: ', error);
29 |
30 | console.log(changes);
31 |
32 | // push changes to supabase
33 | },
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/src/lib/supabase.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage';
2 | import { createClient } from '@supabase/supabase-js';
3 |
4 | const supabaseUrl = 'https://ikpfgvfpwzlfmykwmfzl.supabase.co';
5 | const supabaseAnonKey =
6 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlrcGZndmZwd3psZm15a3dtZnpsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTQxNDQ2MjksImV4cCI6MjAyOTcyMDYyOX0.IsElwb5_dT0s6m1vV69z9z3O0ZC4AdqQ6C_4JozyTP8';
7 |
8 | export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
9 | auth: {
10 | storage: AsyncStorage,
11 | autoRefreshToken: true,
12 | persistSession: true,
13 | detectSessionInUrl: false,
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/src/model/Account.ts:
--------------------------------------------------------------------------------
1 | // model/Post.js
2 | import { Model } from '@nozbe/watermelondb';
3 | import { field, text, nochange, readonly, date } from '@nozbe/watermelondb/decorators';
4 |
5 | export default class Account extends Model {
6 | static table = 'accounts';
7 |
8 | @readonly @date('created_at') createdAt: Date;
9 | @readonly @date('updated_at') updatedAt: Date;
10 |
11 | @text('name') name: string;
12 | @field('cap') cap: number;
13 | @field('tap') tap: number;
14 |
15 | @nochange @field('user_id') userId: string;
16 | }
17 |
--------------------------------------------------------------------------------
/src/model/AccountAllocation.ts:
--------------------------------------------------------------------------------
1 | // model/Post.js
2 | import { Model } from '@nozbe/watermelondb';
3 | import {
4 | field,
5 | text,
6 | readonly,
7 | date,
8 | immutableRelation,
9 | nochange,
10 | } from '@nozbe/watermelondb/decorators';
11 |
12 | export default class AccountAllocation extends Model {
13 | static table = 'account_allocations';
14 | static associations = {
15 | allocations: { type: 'belongs_to', key: 'allocation_id' },
16 | accounts: { type: 'belongs_to', key: 'account_id' },
17 | };
18 |
19 | @readonly @date('created_at') createdAt: Date;
20 | @readonly @date('updated_at') updatedAt: Date;
21 |
22 | @field('cap') cap: number;
23 | @field('amount') amount: number;
24 | @nochange @field('user_id') userId: string;
25 |
26 | @immutableRelation('accounts', 'account_id') account;
27 | @immutableRelation('allocations', 'allocation_id') allocation;
28 | }
29 |
--------------------------------------------------------------------------------
/src/model/Allocation.ts:
--------------------------------------------------------------------------------
1 | // model/Post.js
2 | import { Model } from '@nozbe/watermelondb';
3 | import {
4 | field,
5 | readonly,
6 | date,
7 | children,
8 | nochange,
9 | } from '@nozbe/watermelondb/decorators';
10 |
11 | export default class Allocation extends Model {
12 | static table = 'allocations';
13 | static associations = {
14 | account_allocations: { type: 'has_many', foreignKey: 'allocation_id' },
15 | };
16 |
17 | @field('income') income: number;
18 | @readonly @date('created_at') createdAt: Date;
19 | @readonly @date('updated_at') updatedAt: Date;
20 | @nochange @field('user_id') userId: string;
21 |
22 | @children('account_allocations') accountAllocations;
23 | }
24 |
--------------------------------------------------------------------------------
/src/providers/AuthProvider.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | PropsWithChildren,
3 | createContext,
4 | useContext,
5 | useEffect,
6 | useState,
7 | } from 'react';
8 | import { supabase } from '../lib/supabase';
9 | import { Session, User } from '@supabase/supabase-js';
10 |
11 | type AuthContext = {
12 | session: Session | null;
13 | user: User | null;
14 | isAuthenticated: boolean;
15 | };
16 |
17 | const AuthContext = createContext({
18 | session: null,
19 | user: null,
20 | isAuthenticated: false,
21 | });
22 |
23 | export default function AuthProvider({ children }: PropsWithChildren) {
24 | const [session, setSession] = useState(null);
25 |
26 | useEffect(() => {
27 | supabase.auth.getSession().then(async ({ data: { session } }) => {
28 | setSession(session);
29 | // if (!session) {
30 | // supabase.auth.signInAnonymously();
31 | // }
32 | });
33 |
34 | supabase.auth.onAuthStateChange((_event, session) => {
35 | setSession(session);
36 | });
37 | }, []);
38 |
39 | return (
40 |
47 | {children}
48 |
49 | );
50 | }
51 |
52 | export const useAuth = () => useContext(AuthContext);
53 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the
2 | # working directory name when running `supabase init`.
3 | project_id = "ProfitFirst"
4 |
5 | [api]
6 | enabled = true
7 | # Port to use for the API URL.
8 | port = 54321
9 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
10 | # endpoints. `public` is always included.
11 | schemas = ["public", "graphql_public"]
12 | # Extra schemas to add to the search_path of every request. `public` is always included.
13 | extra_search_path = ["public", "extensions"]
14 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
15 | # for accidental or malicious requests.
16 | max_rows = 1000
17 |
18 | [db]
19 | # Port to use for the local database URL.
20 | port = 54322
21 | # Port used by db diff command to initialize the shadow database.
22 | shadow_port = 54320
23 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
24 | # server_version;` on the remote database to check.
25 | major_version = 15
26 |
27 | [db.pooler]
28 | enabled = false
29 | # Port to use for the local connection pooler.
30 | port = 54329
31 | # Specifies when a server connection can be reused by other clients.
32 | # Configure one of the supported pooler modes: `transaction`, `session`.
33 | pool_mode = "transaction"
34 | # How many server connections to allow per user/database pair.
35 | default_pool_size = 20
36 | # Maximum number of client connections allowed.
37 | max_client_conn = 100
38 |
39 | [realtime]
40 | enabled = true
41 | # Bind realtime via either IPv4 or IPv6. (default: IPv4)
42 | # ip_version = "IPv6"
43 | # The maximum length in bytes of HTTP request headers. (default: 4096)
44 | # max_header_length = 4096
45 |
46 | [studio]
47 | enabled = true
48 | # Port to use for Supabase Studio.
49 | port = 54323
50 | # External URL of the API server that frontend connects to.
51 | api_url = "http://127.0.0.1"
52 | # OpenAI API Key to use for Supabase AI in the Supabase Studio.
53 | openai_api_key = "env(OPENAI_API_KEY)"
54 |
55 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
56 | # are monitored, and you can view the emails that would have been sent from the web interface.
57 | [inbucket]
58 | enabled = true
59 | # Port to use for the email testing server web interface.
60 | port = 54324
61 | # Uncomment to expose additional ports for testing user applications that send emails.
62 | # smtp_port = 54325
63 | # pop3_port = 54326
64 |
65 | [storage]
66 | enabled = true
67 | # The maximum file size allowed (e.g. "5MB", "500KB").
68 | file_size_limit = "50MiB"
69 |
70 | [storage.image_transformation]
71 | enabled = true
72 |
73 | [auth]
74 | enabled = true
75 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
76 | # in emails.
77 | site_url = "http://127.0.0.1:3000"
78 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
79 | additional_redirect_urls = ["https://127.0.0.1:3000"]
80 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
81 | jwt_expiry = 3600
82 | # If disabled, the refresh token will never expire.
83 | enable_refresh_token_rotation = true
84 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
85 | # Requires enable_refresh_token_rotation = true.
86 | refresh_token_reuse_interval = 10
87 | # Allow/disallow new user signups to your project.
88 | enable_signup = true
89 | # Allow/disallow anonymous sign-ins to your project.
90 | enable_anonymous_sign_ins = false
91 | # Allow/disallow testing manual linking of accounts
92 | enable_manual_linking = false
93 |
94 | [auth.email]
95 | # Allow/disallow new user signups via email to your project.
96 | enable_signup = true
97 | # If enabled, a user will be required to confirm any email change on both the old, and new email
98 | # addresses. If disabled, only the new email is required to confirm.
99 | double_confirm_changes = true
100 | # If enabled, users need to confirm their email address before signing in.
101 | enable_confirmations = false
102 | # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
103 | max_frequency = "1s"
104 |
105 | # Uncomment to customize email template
106 | # [auth.email.template.invite]
107 | # subject = "You have been invited"
108 | # content_path = "./supabase/templates/invite.html"
109 |
110 | [auth.sms]
111 | # Allow/disallow new user signups via SMS to your project.
112 | enable_signup = true
113 | # If enabled, users need to confirm their phone number before signing in.
114 | enable_confirmations = false
115 | # Template for sending OTP to users
116 | template = "Your code is {{ .Code }} ."
117 | # Controls the minimum amount of time that must pass before sending another sms otp.
118 | max_frequency = "5s"
119 |
120 | # Use pre-defined map of phone number to OTP for testing.
121 | # [auth.sms.test_otp]
122 | # 4152127777 = "123456"
123 |
124 | # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
125 | # [auth.hook.custom_access_token]
126 | # enabled = true
127 | # uri = "pg-functions:////"
128 |
129 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
130 | [auth.sms.twilio]
131 | enabled = false
132 | account_sid = ""
133 | message_service_sid = ""
134 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
135 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
136 |
137 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
138 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
139 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
140 | [auth.external.apple]
141 | enabled = false
142 | client_id = ""
143 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
144 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
145 | # Overrides the default auth redirectUrl.
146 | redirect_uri = ""
147 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
148 | # or any other third-party OIDC providers.
149 | url = ""
150 | # If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
151 | skip_nonce_check = false
152 |
153 | [analytics]
154 | enabled = false
155 | port = 54327
156 | vector_port = 54328
157 | # Configure one of the supported backends: `postgres`, `bigquery`.
158 | backend = "postgres"
159 |
160 | # Experimental features may be deprecated any time
161 | [experimental]
162 | # Configures Postgres storage engine to use OrioleDB (S3)
163 | orioledb_version = ""
164 | # Configures S3 bucket URL, eg. .s3-.amazonaws.com
165 | s3_host = "env(S3_HOST)"
166 | # Configures S3 bucket region, eg. us-east-1
167 | s3_region = "env(S3_REGION)"
168 | # Configures AWS_ACCESS_KEY_ID for S3 bucket
169 | s3_access_key = "env(S3_ACCESS_KEY)"
170 | # Configures AWS_SECRET_ACCESS_KEY for S3 bucket
171 | s3_secret_key = "env(S3_SECRET_KEY)"
172 |
--------------------------------------------------------------------------------
/supabase/migrations/20240426181512_remote_schema.sql:
--------------------------------------------------------------------------------
1 |
2 | SET statement_timeout = 0;
3 | SET lock_timeout = 0;
4 | SET idle_in_transaction_session_timeout = 0;
5 | SET client_encoding = 'UTF8';
6 | SET standard_conforming_strings = on;
7 | SELECT pg_catalog.set_config('search_path', '', false);
8 | SET check_function_bodies = false;
9 | SET xmloption = content;
10 | SET client_min_messages = warning;
11 | SET row_security = off;
12 |
13 | CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium";
14 |
15 | COMMENT ON SCHEMA "public" IS 'standard public schema';
16 |
17 | CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
18 |
19 | CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
20 |
21 | CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
22 |
23 | CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
24 |
25 | CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
26 |
27 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
28 |
29 | CREATE OR REPLACE FUNCTION "public"."create_account"("_id" "uuid", "_user_id" "uuid", "_name" "text", "_cap" real, "_tap" real, "_created_at" timestamp with time zone, "_updated_at" timestamp with time zone) RETURNS "uuid"
30 | LANGUAGE "plpgsql"
31 | AS $$DECLARE new_id uuid;
32 |
33 | begin
34 | insert into
35 | accounts (
36 | id,
37 | user_id,
38 | name,
39 | cap,
40 | tap,
41 | created_at,
42 | updated_at
43 | )
44 | values (
45 | _id,
46 | _user_id,
47 | _name,
48 | _cap,
49 | _tap,
50 | _created_at,
51 | _updated_at
52 | )
53 | returning id into new_id;
54 |
55 | RETURN new_id;
56 | end;$$;
57 |
58 | ALTER FUNCTION "public"."create_account"("_id" "uuid", "_user_id" "uuid", "_name" "text", "_cap" real, "_tap" real, "_created_at" timestamp with time zone, "_updated_at" timestamp with time zone) OWNER TO "postgres";
59 |
60 | CREATE OR REPLACE FUNCTION "public"."epoch_to_timestamp"("epoch" "text") RETURNS timestamp with time zone
61 | LANGUAGE "plpgsql"
62 | AS $$ begin return timestamp with time zone 'epoch' + ((epoch::bigint) / 1000) * interval '1 second';
63 | end;
64 | $$;
65 |
66 | ALTER FUNCTION "public"."epoch_to_timestamp"("epoch" "text") OWNER TO "postgres";
67 |
68 | CREATE OR REPLACE FUNCTION "public"."pull"("last_pulled_at" bigint, "schemaversion" integer, "migration" "jsonb") RETURNS "jsonb"
69 | LANGUAGE "plpgsql"
70 | AS $$DECLARE _ts timestamp with time zone;
71 | _accounts jsonb;
72 |
73 | BEGIN
74 |
75 | _ts := to_timestamp(last_pulled_at / 1000);
76 |
77 | select jsonb_build_object(
78 | 'created',
79 | '[]'::jsonb,
80 | 'updated',
81 | coalesce(
82 | jsonb_agg(
83 | jsonb_build_object(
84 | 'id',
85 | acc.id,
86 | 'name',
87 | acc.name,
88 | 'cap',
89 | acc.cap,
90 | 'tap',
91 | acc.cap,
92 | 'user_id',
93 | acc.user_id,
94 | 'created_at',
95 | timestamp_to_epoch(acc.created_at),
96 | 'updated_at',
97 | timestamp_to_epoch(acc.updated_at)
98 | )
99 | ) filter (
100 | where acc.deleted_at is null
101 | and acc.updated_at > _ts
102 | ),
103 | '[]'::jsonb
104 | ),
105 | 'deleted',
106 | coalesce(
107 | jsonb_agg(to_jsonb(acc.id)) filter (
108 | where acc.deleted_at is not null
109 | and acc.updated_at > _ts
110 | ),
111 | '[]'::jsonb
112 | )
113 | ) into _accounts
114 | from accounts acc;
115 |
116 | return jsonb_build_object(
117 | 'changes',
118 | jsonb_build_object(
119 | 'accounts',
120 | _accounts
121 | ),
122 | 'timestamp',
123 | timestamp_to_epoch(now())
124 | );
125 |
126 | END;$$;
127 |
128 | ALTER FUNCTION "public"."pull"("last_pulled_at" bigint, "schemaversion" integer, "migration" "jsonb") OWNER TO "postgres";
129 |
130 | CREATE OR REPLACE FUNCTION "public"."push"("changes" "jsonb") RETURNS "void"
131 | LANGUAGE "plpgsql"
132 | AS $$declare new_account jsonb;
133 |
134 | BEGIN
135 | for new_account in
136 | select jsonb_array_elements((changes->'accounts'->'created')) loop perform create_account(
137 | (new_account->>'id')::uuid,
138 | (new_account->>'user_id')::uuid,
139 | (new_account->>'name'),
140 | (new_account->>'cap')::float4,
141 | (new_account->>'tap')::float4,
142 | epoch_to_timestamp(new_account->>'created_at'),
143 | epoch_to_timestamp(new_account->>'updated_at')
144 | );
145 | end loop;
146 |
147 | with changes_data as (
148 | select jsonb_array_elements_text(changes->'accounts'->'deleted')::uuid as deleted
149 | )
150 | UPDATE accounts
151 | set deleted_at = now(),
152 | updated_at = now()
153 | from changes_data
154 | where accounts.id = changes_data.deleted;
155 |
156 | END;$$;
157 |
158 | ALTER FUNCTION "public"."push"("changes" "jsonb") OWNER TO "postgres";
159 |
160 | CREATE OR REPLACE FUNCTION "public"."timestamp_to_epoch"("ts" timestamp with time zone) RETURNS bigint
161 | LANGUAGE "plpgsql"
162 | AS $$ begin return (
163 | extract(
164 | epoch
165 | from ts
166 | ) * 1000
167 | )::bigint;
168 | end;
169 | $$;
170 |
171 | ALTER FUNCTION "public"."timestamp_to_epoch"("ts" timestamp with time zone) OWNER TO "postgres";
172 |
173 | SET default_tablespace = '';
174 |
175 | SET default_table_access_method = "heap";
176 |
177 | CREATE TABLE IF NOT EXISTS "public"."account_allocations" (
178 | "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
179 | "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
180 | "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
181 | "amount" double precision DEFAULT '0'::double precision NOT NULL,
182 | "cap" real DEFAULT '0'::real NOT NULL,
183 | "account_id" "uuid" NOT NULL,
184 | "allocation_id" "uuid" NOT NULL,
185 | "user_id" "uuid" NOT NULL
186 | );
187 |
188 | ALTER TABLE "public"."account_allocations" OWNER TO "postgres";
189 |
190 | CREATE TABLE IF NOT EXISTS "public"."accounts" (
191 | "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
192 | "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
193 | "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
194 | "name" "text" NOT NULL,
195 | "cap" real DEFAULT '0'::real NOT NULL,
196 | "tap" real DEFAULT '0'::real NOT NULL,
197 | "user_id" "uuid" NOT NULL,
198 | "deleted_at" timestamp with time zone
199 | );
200 |
201 | ALTER TABLE "public"."accounts" OWNER TO "postgres";
202 |
203 | CREATE TABLE IF NOT EXISTS "public"."allocations" (
204 | "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
205 | "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
206 | "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
207 | "income" double precision DEFAULT '0'::double precision NOT NULL,
208 | "user_id" "uuid" NOT NULL
209 | );
210 |
211 | ALTER TABLE "public"."allocations" OWNER TO "postgres";
212 |
213 | ALTER TABLE ONLY "public"."account_allocations"
214 | ADD CONSTRAINT "account_allocations_pkey" PRIMARY KEY ("id");
215 |
216 | ALTER TABLE ONLY "public"."accounts"
217 | ADD CONSTRAINT "accounts_pkey" PRIMARY KEY ("id");
218 |
219 | ALTER TABLE ONLY "public"."allocations"
220 | ADD CONSTRAINT "allocations_pkey" PRIMARY KEY ("id");
221 |
222 | ALTER TABLE ONLY "public"."account_allocations"
223 | ADD CONSTRAINT "account_allocations_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id");
224 |
225 | ALTER TABLE ONLY "public"."account_allocations"
226 | ADD CONSTRAINT "account_allocations_allocation_id_fkey" FOREIGN KEY ("allocation_id") REFERENCES "public"."allocations"("id");
227 |
228 | ALTER TABLE ONLY "public"."account_allocations"
229 | ADD CONSTRAINT "account_allocations_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id");
230 |
231 | ALTER TABLE ONLY "public"."accounts"
232 | ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id");
233 |
234 | ALTER TABLE ONLY "public"."allocations"
235 | ADD CONSTRAINT "allocations_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id");
236 |
237 | CREATE POLICY "ALL PUBLIC" ON "public"."accounts" USING (true);
238 |
239 | CREATE POLICY "Enable delete for users based on user_id" ON "public"."accounts" FOR DELETE USING ((( SELECT "auth"."uid"() AS "uid") = "user_id"));
240 |
241 | CREATE POLICY "Enable insert for ANYONE (delete me later)" ON "public"."accounts" FOR INSERT TO "authenticated", "anon", "postgres" WITH CHECK (true);
242 |
243 | CREATE POLICY "Enable insert for authenticated users only" ON "public"."accounts" FOR INSERT TO "authenticated" WITH CHECK (true);
244 |
245 | CREATE POLICY "Enable update for users based on id" ON "public"."accounts" FOR UPDATE USING ((( SELECT "auth"."uid"() AS "uid") = "user_id")) WITH CHECK ((( SELECT "auth"."uid"() AS "uid") = "user_id"));
246 |
247 | ALTER TABLE "public"."account_allocations" ENABLE ROW LEVEL SECURITY;
248 |
249 | ALTER TABLE "public"."accounts" ENABLE ROW LEVEL SECURITY;
250 |
251 | ALTER TABLE "public"."allocations" ENABLE ROW LEVEL SECURITY;
252 |
253 | ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres";
254 |
255 | GRANT USAGE ON SCHEMA "public" TO "postgres";
256 | GRANT USAGE ON SCHEMA "public" TO "anon";
257 | GRANT USAGE ON SCHEMA "public" TO "authenticated";
258 | GRANT USAGE ON SCHEMA "public" TO "service_role";
259 |
260 | GRANT ALL ON FUNCTION "public"."create_account"("_id" "uuid", "_user_id" "uuid", "_name" "text", "_cap" real, "_tap" real, "_created_at" timestamp with time zone, "_updated_at" timestamp with time zone) TO "anon";
261 | GRANT ALL ON FUNCTION "public"."create_account"("_id" "uuid", "_user_id" "uuid", "_name" "text", "_cap" real, "_tap" real, "_created_at" timestamp with time zone, "_updated_at" timestamp with time zone) TO "authenticated";
262 | GRANT ALL ON FUNCTION "public"."create_account"("_id" "uuid", "_user_id" "uuid", "_name" "text", "_cap" real, "_tap" real, "_created_at" timestamp with time zone, "_updated_at" timestamp with time zone) TO "service_role";
263 |
264 | GRANT ALL ON FUNCTION "public"."epoch_to_timestamp"("epoch" "text") TO "anon";
265 | GRANT ALL ON FUNCTION "public"."epoch_to_timestamp"("epoch" "text") TO "authenticated";
266 | GRANT ALL ON FUNCTION "public"."epoch_to_timestamp"("epoch" "text") TO "service_role";
267 |
268 | GRANT ALL ON FUNCTION "public"."pull"("last_pulled_at" bigint, "schemaversion" integer, "migration" "jsonb") TO "anon";
269 | GRANT ALL ON FUNCTION "public"."pull"("last_pulled_at" bigint, "schemaversion" integer, "migration" "jsonb") TO "authenticated";
270 | GRANT ALL ON FUNCTION "public"."pull"("last_pulled_at" bigint, "schemaversion" integer, "migration" "jsonb") TO "service_role";
271 |
272 | GRANT ALL ON FUNCTION "public"."push"("changes" "jsonb") TO "anon";
273 | GRANT ALL ON FUNCTION "public"."push"("changes" "jsonb") TO "authenticated";
274 | GRANT ALL ON FUNCTION "public"."push"("changes" "jsonb") TO "service_role";
275 |
276 | GRANT ALL ON FUNCTION "public"."timestamp_to_epoch"("ts" timestamp with time zone) TO "anon";
277 | GRANT ALL ON FUNCTION "public"."timestamp_to_epoch"("ts" timestamp with time zone) TO "authenticated";
278 | GRANT ALL ON FUNCTION "public"."timestamp_to_epoch"("ts" timestamp with time zone) TO "service_role";
279 |
280 | GRANT ALL ON TABLE "public"."account_allocations" TO "anon";
281 | GRANT ALL ON TABLE "public"."account_allocations" TO "authenticated";
282 | GRANT ALL ON TABLE "public"."account_allocations" TO "service_role";
283 |
284 | GRANT ALL ON TABLE "public"."accounts" TO "anon";
285 | GRANT ALL ON TABLE "public"."accounts" TO "authenticated";
286 | GRANT ALL ON TABLE "public"."accounts" TO "service_role";
287 |
288 | GRANT ALL ON TABLE "public"."allocations" TO "anon";
289 | GRANT ALL ON TABLE "public"."allocations" TO "authenticated";
290 | GRANT ALL ON TABLE "public"."allocations" TO "service_role";
291 |
292 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres";
293 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon";
294 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated";
295 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role";
296 |
297 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres";
298 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon";
299 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated";
300 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role";
301 |
302 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres";
303 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon";
304 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated";
305 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role";
306 |
307 | RESET ALL;
308 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notJust-dev/ProfitFirst/7e4b569c396a7a06af5bdef0d16165296cf5ad52/supabase/seed.sql
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true
4 | },
5 | "extends": "expo/tsconfig.base"
6 | }
7 |
--------------------------------------------------------------------------------