diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 85808d64..41ccc750 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -142,11 +142,27 @@ jobs: limit-access-to-actor: true # Save the build artifacts to be used in other jobs + - name: List APKs before upload + run: | + echo "Available APKs in build directory:" + find android/${{ env.main_project_module }}/build/outputs/apk/ -type f -name "*.apk" | sort + - name: Upload APK artifacts uses: actions/upload-artifact@v4 with: name: android-apk-artifacts-${{ matrix.arch }} - path: android/${{ env.main_project_module }}/build/outputs/apk/ + path: | + android/${{ env.main_project_module }}/build/outputs/apk/${{ matrix.arch == 'arm64-v8a' && 'arm64v8a' || 'x8664' }}/debug/*-debug.apk + android/${{ env.main_project_module }}/build/outputs/apk/${{ matrix.arch == 'arm64-v8a' && 'arm64v8a' || 'x8664' }}/release/*-release-unsigned.apk + retention-days: 1 + if-no-files-found: error + + # Save the test APKs separately + - name: Upload Test APK artifacts + uses: actions/upload-artifact@v4 + with: + name: android-test-apk-artifacts-${{ matrix.arch }} + path: android/${{ env.main_project_module }}/build/outputs/apk/androidTest/${{ matrix.arch == 'arm64-v8a' && 'arm64v8a' || 'x8664' }}/debug/ retention-days: 1 if-no-files-found: error @@ -604,7 +620,13 @@ jobs: with: name: android-apk-artifacts-${{ matrix.arch }} path: android/app/build/outputs/apk - + + - name: Download Test APK artifacts + uses: actions/download-artifact@v4 + with: + name: android-test-apk-artifacts-${{ matrix.arch }} + path: android/app/build/outputs/apk/androidTest + - name: Download Coverage artifacts uses: actions/download-artifact@v4 with: diff --git a/App/SetupSync.tsx b/App/SetupSync.tsx index 30d8b46e..2782a4bc 100644 --- a/App/SetupSync.tsx +++ b/App/SetupSync.tsx @@ -5,9 +5,10 @@ import { open } from '@op-engineering/op-sqlite'; import { useQuery } from '@powersync/react'; import { PowerSyncContext } from "@powersync/react"; import { installCrsqliteOnTable } from '@lib/cr-sqlite/install'; -import { psInsertDbTable, psClearTable } from '@lib/orm'; +import { psInsertDbTable, supabaseClearTable } from '@lib/orm'; import { useNavigation } from '@react-navigation/native'; import { useSettings } from '@lib/hooks/SettingsContext'; +import { useSupabase } from '@lib/hooks/SupabaseContext'; import { GITHUB_README_URL } from '@lib/constants'; // Split out type imports for better readability @@ -20,6 +21,7 @@ type NavigationProp = NativeStackNavigationProp; export const SetupSync = () => { const navigation = useNavigation(); const { settings } = useSettings(); + const { supabaseClient } = useSupabase(); const debugDisplayKeys = ['id', 'ttl', 'istart' ,'loc']; const [showDangerZone, setShowDangerZone] = useState(false); const [showDebugOutput, setShowDebugOutput] = useState(false); @@ -236,7 +238,7 @@ export const SetupSync = () => { style={[styles.syncButton, styles.deleteButton]} onPress={async () => { try { - await psClearTable('eventsV9', providerDb); + await supabaseClearTable('eventsV9', supabaseClient); } catch (error) { console.error('Failed to clear PowerSync events:', error); } diff --git a/App/index.tsx b/App/index.tsx index 893e0497..37e9972b 100644 --- a/App/index.tsx +++ b/App/index.tsx @@ -3,7 +3,8 @@ import { StyleSheet, Text, View, Button, TouchableOpacity, BackHandler, Linking import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { db as psDb, setupPowerSync } from '@lib/powersync'; +import { db as psDb } from '@lib/powersync'; +import { setupRemoteDatabaseConnections } from '@lib/init_remote_db_connections'; import Logger from 'js-logger'; import { PowerSyncContext } from "@powersync/react"; import { SetupSync } from './SetupSync'; @@ -12,6 +13,8 @@ import { enableScreens } from 'react-native-screens'; import { useSettings } from '@lib/hooks/SettingsContext'; import { Ionicons } from '@expo/vector-icons'; import { SettingsProvider } from '@lib/hooks/SettingsContext'; +import { SupabaseProvider } from '@lib/hooks/SupabaseContext'; +import { SupabaseClient } from '@supabase/supabase-js'; import { GITHUB_README_URL } from '@lib/constants'; // Enable screens @@ -47,11 +50,13 @@ const InitialSetupScreen = ({ navigation }: { navigation: NativeStackNavigationP const HomeScreen = ({ navigation }: { navigation: NativeStackNavigationProp }) => { const { settings } = useSettings(); const [isReady, setIsReady] = useState(false); + const [initializedSupabaseClient, setInitializedSupabaseClient] = useState(null); useEffect(() => { const init = async () => { if (settings.syncEnabled) { - await setupPowerSync(settings); + const { supabaseClient } = await setupRemoteDatabaseConnections(settings, psDb); + setInitializedSupabaseClient(supabaseClient); } setIsReady(true); }; @@ -66,7 +71,11 @@ const HomeScreen = ({ navigation }: { navigation: NativeStackNavigationProp : ; + return ( + + {settings.syncEnabled ? : } + + ); }; export const App = () => { diff --git a/lib/hooks/SupabaseContext.tsx b/lib/hooks/SupabaseContext.tsx new file mode 100644 index 00000000..f6e55e40 --- /dev/null +++ b/lib/hooks/SupabaseContext.tsx @@ -0,0 +1,29 @@ +import React, { createContext, useContext } from 'react'; +import { SupabaseClient } from '@supabase/supabase-js'; + +interface SupabaseContextType { + supabaseClient: SupabaseClient | null; +} + +const SupabaseContext = createContext({ supabaseClient: null }); + +export const useSupabase = () => { + const context = useContext(SupabaseContext); + if (!context) { + throw new Error('useSupabase must be used within a SupabaseProvider'); + } + return context; +}; + +interface SupabaseProviderProps { + children: React.ReactNode; + client: SupabaseClient | null; +} + +export const SupabaseProvider: React.FC = ({ children, client }) => { + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/lib/init_remote_db_connections.ts b/lib/init_remote_db_connections.ts new file mode 100644 index 00000000..6013ce82 --- /dev/null +++ b/lib/init_remote_db_connections.ts @@ -0,0 +1,33 @@ +import 'react-native-url-polyfill/auto' +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { PowerSyncDatabase } from '@powersync/react-native'; +import { Connector } from './powersync/Connector'; +import { Settings } from './hooks/SettingsContext'; + +export async function setupRemoteDatabaseConnections( + settings: Settings, + powerSyncDb: PowerSyncDatabase +) { + // Initialize Supabase client + let supabaseClient: SupabaseClient | undefined = undefined; + try { + supabaseClient = createClient(settings.supabaseUrl, settings.supabaseAnonKey); + console.log('Supabase client initialized successfully'); + } catch (e) { + console.error('Error initializing Supabase client:', e); + } + + // Initialize PowerSync connector with Supabase client + const connector = new Connector(settings, supabaseClient!); + powerSyncDb.connect(connector); + + try { + await powerSyncDb.init(); + } catch (e) { + console.log('Error initializing PowerSync connection:', e); + } + + return { + supabaseClient + }; +} \ No newline at end of file diff --git a/lib/orm/index.ts b/lib/orm/index.ts index ee854dd3..8e768fe3 100644 --- a/lib/orm/index.ts +++ b/lib/orm/index.ts @@ -1,5 +1,11 @@ import { open } from '@op-engineering/op-sqlite'; import { AbstractPowerSyncDatabase } from '@powersync/react-native'; +import { SupabaseClient } from '@supabase/supabase-js'; + +/** + * TODO: refactor the orm to be a hook that includes the supabase client and the powersync db + * without having to pass the supabase client and the powersync db to every function + */ /** * Inserts data from a regular SQLite table into a PowerSync table @@ -56,6 +62,14 @@ export async function psClearTable( tableName: string, psDb: AbstractPowerSyncDatabase ) { + // TODO + // so sqlite doesn't support TRUNCATE + // and even if it did that isn't supported by powersync's protocol + // https://docs.powersync.com/architecture/powersync-protocol + // https://docs.powersync.com/architecture/client-architecture + // so I think we can just use the supabase client to TRUNCATE the table + // and still DELETE FROM to clear local + // honestly the sync server should just handle from the truncate try { const deleteResult = await psDb.execute(`DELETE FROM ${tableName}`); console.log(`Successfully cleared all records from PowerSync table ${tableName}`); @@ -66,3 +80,20 @@ export async function psClearTable( } } + +export async function supabaseClearTable( + tableName: string, + supabaseClient: SupabaseClient | null +) { + if (!supabaseClient) { + throw new Error('Supabase client is not initialized'); + } + + try { + await supabaseClient.from(tableName).delete().neq('id', 0); + console.log(`Successfully cleared all records from Supabase table ${tableName}`); + } catch (error) { + console.error(`Failed to clear Supabase table ${tableName}:`, error); + throw error; + } +} \ No newline at end of file diff --git a/lib/powersync/Connector.ts b/lib/powersync/Connector.ts index 0324dcc1..52f6b8cf 100644 --- a/lib/powersync/Connector.ts +++ b/lib/powersync/Connector.ts @@ -25,12 +25,9 @@ export class Connector implements PowerSyncBackendConnector { client: SupabaseClient; private settings: Settings; - constructor(settings: Settings) { + constructor(settings: Settings, supabaseClient: SupabaseClient) { this.settings = settings; - // TODO setup session storage to support supabase auth - // right now its not needed because will have people input - // there own powersync token an supabase links in the app to start - this.client = createClient(settings.supabaseUrl, settings.supabaseAnonKey); + this.client = supabaseClient; } async fetchCredentials() { diff --git a/lib/powersync/index.tsx b/lib/powersync/index.tsx index c42db26d..75d20e94 100644 --- a/lib/powersync/index.tsx +++ b/lib/powersync/index.tsx @@ -1,8 +1,7 @@ import { OPSqliteOpenFactory } from '@powersync/op-sqlite'; import { PowerSyncDatabase } from '@powersync/react-native'; -import { Connector } from './Connector'; import { AppSchema } from './Schema'; -import { Settings } from '../hooks/useStoredSettings'; + const factory = new OPSqliteOpenFactory( { // Filename for the SQLite database — it's important to only instantiate one instance per file. @@ -19,18 +18,4 @@ export const db = new PowerSyncDatabase({ // The schema you defined in the previous step schema: AppSchema, database: factory, -}); - -export const setupPowerSync = async (settings: Settings) => { - // Uses the backend connector that will be created in the next section - const connector = new Connector(settings); - db.connect(connector); - - try { - await db.init(); - } catch (e) { - console.log('Error initializing PowerSync connection:', e); - } - - // console.log(db) -}; \ No newline at end of file +}); \ No newline at end of file