feat: add mobile git transport boundary
This commit is contained in:
@@ -65,6 +65,7 @@ import type { MobileVaultRuntime } from './mobileVaultRuntime'
|
||||
import { useMobileVaultRuntimeLoader } from './useMobileVaultRuntimeLoader'
|
||||
import type { MobileNotePropertyPatch } from './mobileNoteProperties'
|
||||
import { useMobileGitSyncFlow } from './useMobileGitSyncFlow'
|
||||
import { createUnavailableMobileGitTransport } from './mobileGitTransport'
|
||||
|
||||
export function MobileApp() {
|
||||
const { width } = useWindowDimensions()
|
||||
@@ -72,6 +73,7 @@ export function MobileApp() {
|
||||
const showsProperties = width >= 1000
|
||||
const appStateStorage = useMemo(() => createNativeMobileAppStateStorage(), [])
|
||||
const gitCredentialStorage = useMemo(() => createNativeMobileGitCredentialStorage(), [])
|
||||
const gitTransport = useMemo(() => createUnavailableMobileGitTransport(), [])
|
||||
const gitHubOAuthClientIdState = useMemo(() => currentMobileGitHubOAuthClientIdState(), [])
|
||||
const vaultMetadataStorage = useMemo(() => createNativeMobileVaultMetadataStorage(), [])
|
||||
const [activeVaultMetadata, setActiveVaultMetadata] = useState(defaultMobileVaultMetadata)
|
||||
@@ -86,6 +88,7 @@ export function MobileApp() {
|
||||
const gitSyncFlow = useMobileGitSyncFlow({
|
||||
createGitHubOAuthSession: createNativeMobileGitHubOAuthSessionFromEnvironment,
|
||||
credentialStorage: gitCredentialStorage,
|
||||
gitTransport,
|
||||
vault: activeVaultMetadata,
|
||||
})
|
||||
const autosaveQueue = useMemo(
|
||||
@@ -173,7 +176,7 @@ export function MobileApp() {
|
||||
onCancelCreateNote={createFlow.cancel}
|
||||
onChangeCreateNoteTitle={createFlow.setTitle}
|
||||
onOpenCreateNote={createFlow.open}
|
||||
onGitSyncAction={gitSyncFlow.authenticate}
|
||||
onGitSyncAction={gitSyncFlow.runPrimaryAction}
|
||||
onRetryRuntimeLoad={runtimeLoader.retry}
|
||||
onSubmitCreateNote={createFlow.submit}
|
||||
onSelectNote={selectNote}
|
||||
@@ -213,7 +216,7 @@ export function MobileApp() {
|
||||
onCancelCreateNote={createFlow.cancel}
|
||||
onChangeCreateNoteTitle={createFlow.setTitle}
|
||||
onOpenCreateNote={createFlow.open}
|
||||
onGitSyncAction={gitSyncFlow.authenticate}
|
||||
onGitSyncAction={gitSyncFlow.runPrimaryAction}
|
||||
onRetryRuntimeLoad={runtimeLoader.retry}
|
||||
onSubmitCreateNote={createFlow.submit}
|
||||
onSelectNote={selectNote}
|
||||
|
||||
70
apps/mobile/src/mobileGitPrimaryAction.test.ts
Normal file
70
apps/mobile/src/mobileGitPrimaryAction.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mobileGitPrimaryActionForPlan } from './mobileGitPrimaryAction'
|
||||
import type { MobileGitRemote } from './mobileGitRemote'
|
||||
|
||||
describe('mobile git primary action', () => {
|
||||
it('routes missing credentials to authentication', () => {
|
||||
expect(mobileGitPrimaryActionForPlan({
|
||||
authStrategy: 'githubOAuth',
|
||||
host: 'github.com',
|
||||
primaryAction: 'authenticate',
|
||||
state: 'authRequired',
|
||||
})).toEqual({ state: 'authenticate' })
|
||||
})
|
||||
|
||||
it('routes ready pull and push states to transport execution', () => {
|
||||
expect(mobileGitPrimaryActionForPlan({
|
||||
canPull: true,
|
||||
canPush: false,
|
||||
primaryAction: 'pull',
|
||||
remote: remote(),
|
||||
state: 'ready',
|
||||
})).toMatchObject({ operation: 'pull', state: 'transport' })
|
||||
|
||||
expect(mobileGitPrimaryActionForPlan({
|
||||
canPull: true,
|
||||
canPush: true,
|
||||
primaryAction: 'push',
|
||||
remote: remote(),
|
||||
state: 'ready',
|
||||
})).toMatchObject({ operation: 'push', state: 'transport' })
|
||||
})
|
||||
|
||||
it('retries clone failures through authentication and sync failures through transport', () => {
|
||||
expect(mobileGitPrimaryActionForPlan({
|
||||
message: 'GitHub authentication failed.',
|
||||
operation: 'clone',
|
||||
primaryAction: 'retry',
|
||||
remote: remote(),
|
||||
state: 'failed',
|
||||
})).toEqual({ state: 'authenticate' })
|
||||
|
||||
expect(mobileGitPrimaryActionForPlan({
|
||||
message: 'Pull failed.',
|
||||
operation: 'pull',
|
||||
primaryAction: 'retry',
|
||||
remote: remote(),
|
||||
state: 'failed',
|
||||
})).toMatchObject({ operation: 'pull', state: 'transport' })
|
||||
})
|
||||
|
||||
it('ignores local-only and already syncing plans', () => {
|
||||
expect(mobileGitPrimaryActionForPlan({ primaryAction: null, state: 'localOnly' })).toEqual({ state: 'ignored' })
|
||||
expect(mobileGitPrimaryActionForPlan({
|
||||
operation: 'pull',
|
||||
primaryAction: null,
|
||||
remote: remote(),
|
||||
state: 'syncing',
|
||||
})).toEqual({ state: 'ignored' })
|
||||
})
|
||||
})
|
||||
|
||||
function remote(): MobileGitRemote {
|
||||
return {
|
||||
authStrategy: 'githubOAuth',
|
||||
host: 'github.com',
|
||||
owner: 'refactoringhq',
|
||||
repository: 'tolaria',
|
||||
url: 'https://github.com/refactoringhq/tolaria.git',
|
||||
}
|
||||
}
|
||||
52
apps/mobile/src/mobileGitPrimaryAction.ts
Normal file
52
apps/mobile/src/mobileGitPrimaryAction.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { MobileGitOperation, MobileGitSyncPlan } from './mobileGitSyncPlan'
|
||||
import type { MobileGitRemote } from './mobileGitRemote'
|
||||
|
||||
export type MobileGitTransportOperation = Exclude<MobileGitOperation, 'clone'>
|
||||
|
||||
export type MobileGitPrimaryAction =
|
||||
| {
|
||||
state: 'authenticate'
|
||||
}
|
||||
| {
|
||||
operation: MobileGitTransportOperation
|
||||
remote: MobileGitRemote
|
||||
state: 'transport'
|
||||
}
|
||||
| {
|
||||
state: 'ignored'
|
||||
}
|
||||
|
||||
export function mobileGitPrimaryActionForPlan(plan: MobileGitSyncPlan): MobileGitPrimaryAction {
|
||||
if (plan.state === 'authRequired') {
|
||||
return { state: 'authenticate' }
|
||||
}
|
||||
|
||||
if (plan.state === 'ready') {
|
||||
return transportAction({
|
||||
operation: plan.primaryAction,
|
||||
remote: plan.remote,
|
||||
})
|
||||
}
|
||||
|
||||
if (plan.state === 'failed') {
|
||||
return plan.operation === 'clone'
|
||||
? { state: 'authenticate' }
|
||||
: transportAction({ operation: plan.operation, remote: plan.remote })
|
||||
}
|
||||
|
||||
return { state: 'ignored' }
|
||||
}
|
||||
|
||||
function transportAction({
|
||||
operation,
|
||||
remote,
|
||||
}: {
|
||||
operation: MobileGitTransportOperation
|
||||
remote: MobileGitRemote
|
||||
}): MobileGitPrimaryAction {
|
||||
return {
|
||||
operation,
|
||||
remote,
|
||||
state: 'transport',
|
||||
}
|
||||
}
|
||||
99
apps/mobile/src/mobileGitSyncFlowAction.test.ts
Normal file
99
apps/mobile/src/mobileGitSyncFlowAction.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runMobileGitSyncFlowAction, type MobileGitSyncFlowFailure } from './mobileGitSyncFlowAction'
|
||||
import type { MobileGitOperation, MobileGitSyncPlan } from './mobileGitSyncPlan'
|
||||
import type { MobileGitRemote } from './mobileGitRemote'
|
||||
|
||||
describe('mobile git sync flow action', () => {
|
||||
it('ignores actions while another operation is active', () => {
|
||||
const events: string[] = []
|
||||
|
||||
runMobileGitSyncFlowAction({
|
||||
...baseActionInput(events),
|
||||
activeOperation: 'pull',
|
||||
})
|
||||
|
||||
expect(events).toEqual([])
|
||||
})
|
||||
|
||||
it('runs ready pull and records transport failures', async () => {
|
||||
const events: string[] = []
|
||||
const failures: Array<MobileGitSyncFlowFailure | null> = []
|
||||
|
||||
runMobileGitSyncFlowAction({
|
||||
...baseActionInput(events),
|
||||
gitSyncPlan: readyPlan(),
|
||||
gitTransport: {
|
||||
pull: async () => ({ message: 'Pull failed.', state: 'failed' }),
|
||||
push: async () => ({ state: 'completed' }),
|
||||
},
|
||||
setFailure: (failure) => failures.push(failure),
|
||||
})
|
||||
await Promise.resolve()
|
||||
|
||||
expect(events).toEqual(['active:pull'])
|
||||
expect(failures).toEqual([null, { message: 'Pull failed.', operation: 'pull' }])
|
||||
})
|
||||
|
||||
it('routes auth-required plans to the authentication operation', () => {
|
||||
const events: string[] = []
|
||||
|
||||
runMobileGitSyncFlowAction({
|
||||
...baseActionInput(events),
|
||||
gitSyncPlan: {
|
||||
authStrategy: 'githubOAuth',
|
||||
host: 'github.com',
|
||||
primaryAction: 'authenticate',
|
||||
state: 'authRequired',
|
||||
},
|
||||
})
|
||||
|
||||
expect(events).toEqual(['active:clone'])
|
||||
})
|
||||
})
|
||||
|
||||
function baseActionInput(events: string[]) {
|
||||
return {
|
||||
activeOperation: null,
|
||||
credentialStorage: {
|
||||
loadState: async () => ({ state: 'missing' as const }),
|
||||
remove: async () => {},
|
||||
saveRecord: async () => {},
|
||||
},
|
||||
createGitHubOAuthSession: () => ({
|
||||
authorize: async () => ({ state: 'cancelled' as const }),
|
||||
}),
|
||||
gitSyncPlan: { primaryAction: null, state: 'localOnly' } satisfies MobileGitSyncPlan,
|
||||
gitTransport: {
|
||||
pull: async () => ({ state: 'completed' as const }),
|
||||
push: async () => ({ state: 'completed' as const }),
|
||||
},
|
||||
refreshCredentials: () => events.push('refresh'),
|
||||
setActiveOperation: (operation: MobileGitOperation | null) => {
|
||||
if (operation) {
|
||||
events.push(`active:${operation}`)
|
||||
}
|
||||
},
|
||||
setFailure: () => {},
|
||||
vault: { id: 'personal', name: 'Personal Journal' },
|
||||
}
|
||||
}
|
||||
|
||||
function readyPlan(): MobileGitSyncPlan {
|
||||
return {
|
||||
canPull: true,
|
||||
canPush: false,
|
||||
primaryAction: 'pull',
|
||||
remote: remote(),
|
||||
state: 'ready',
|
||||
}
|
||||
}
|
||||
|
||||
function remote(): MobileGitRemote {
|
||||
return {
|
||||
authStrategy: 'githubOAuth',
|
||||
host: 'github.com',
|
||||
owner: 'refactoringhq',
|
||||
repository: 'tolaria',
|
||||
url: 'https://github.com/refactoringhq/tolaria.git',
|
||||
}
|
||||
}
|
||||
134
apps/mobile/src/mobileGitSyncFlowAction.ts
Normal file
134
apps/mobile/src/mobileGitSyncFlowAction.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { authenticateMobileGitSyncPlan } from './mobileGitAuthentication'
|
||||
import type { MobileGitHubOAuthSession } from './mobileGitHubOAuthFlow'
|
||||
import { mobileGitPrimaryActionForPlan } from './mobileGitPrimaryAction'
|
||||
import type { MobileGitCredentialStorage } from './mobileGitCredentialStorage'
|
||||
import type { MobileGitOperation, MobileGitSyncPlan } from './mobileGitSyncPlan'
|
||||
import {
|
||||
runMobileGitTransportOperation,
|
||||
type MobileGitTransport,
|
||||
} from './mobileGitTransport'
|
||||
import type { MobileVaultMetadata } from './mobileVaultMetadata'
|
||||
|
||||
export type MobileGitSyncFlowFailure = {
|
||||
message: string
|
||||
operation: MobileGitOperation
|
||||
}
|
||||
|
||||
export function runMobileGitSyncFlowAction({
|
||||
activeOperation,
|
||||
credentialStorage,
|
||||
createGitHubOAuthSession,
|
||||
gitSyncPlan,
|
||||
gitTransport,
|
||||
refreshCredentials,
|
||||
setActiveOperation,
|
||||
setFailure,
|
||||
vault,
|
||||
}: {
|
||||
activeOperation: MobileGitOperation | null
|
||||
credentialStorage: MobileGitCredentialStorage
|
||||
createGitHubOAuthSession: () => MobileGitHubOAuthSession
|
||||
gitSyncPlan: MobileGitSyncPlan
|
||||
gitTransport: MobileGitTransport
|
||||
refreshCredentials: () => void
|
||||
setActiveOperation: (operation: MobileGitOperation | null) => void
|
||||
setFailure: (failure: MobileGitSyncFlowFailure | null) => void
|
||||
vault: MobileVaultMetadata
|
||||
}) {
|
||||
if (activeOperation) {
|
||||
return
|
||||
}
|
||||
|
||||
const action = mobileGitPrimaryActionForPlan(gitSyncPlan)
|
||||
if (action.state === 'authenticate') {
|
||||
runAuthenticationAction({
|
||||
credentialStorage,
|
||||
createGitHubOAuthSession,
|
||||
gitSyncPlan,
|
||||
refreshCredentials,
|
||||
setActiveOperation,
|
||||
setFailure,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (action.state === 'transport') {
|
||||
runTransportAction({
|
||||
gitTransport,
|
||||
operation: action.operation,
|
||||
remote: action.remote,
|
||||
setActiveOperation,
|
||||
setFailure,
|
||||
vault,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function runAuthenticationAction({
|
||||
credentialStorage,
|
||||
createGitHubOAuthSession,
|
||||
gitSyncPlan,
|
||||
refreshCredentials,
|
||||
setActiveOperation,
|
||||
setFailure,
|
||||
}: {
|
||||
credentialStorage: MobileGitCredentialStorage
|
||||
createGitHubOAuthSession: () => MobileGitHubOAuthSession
|
||||
gitSyncPlan: MobileGitSyncPlan
|
||||
refreshCredentials: () => void
|
||||
setActiveOperation: (operation: MobileGitOperation | null) => void
|
||||
setFailure: (failure: MobileGitSyncFlowFailure | null) => void
|
||||
}) {
|
||||
setFailure(null)
|
||||
setActiveOperation('clone')
|
||||
void authenticateMobileGitSyncPlan({
|
||||
credentialStorage,
|
||||
createGitHubOAuthSession,
|
||||
now: () => new Date().toISOString(),
|
||||
plan: gitSyncPlan,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.state === 'connected') {
|
||||
refreshCredentials()
|
||||
return
|
||||
}
|
||||
|
||||
if (result.state === 'failed') {
|
||||
setFailure({ message: result.message, operation: 'clone' })
|
||||
}
|
||||
})
|
||||
.catch(() => setFailure({ message: 'GitHub authentication failed.', operation: 'clone' }))
|
||||
.finally(() => setActiveOperation(null))
|
||||
}
|
||||
|
||||
function runTransportAction({
|
||||
gitTransport,
|
||||
operation,
|
||||
remote,
|
||||
setActiveOperation,
|
||||
setFailure,
|
||||
vault,
|
||||
}: {
|
||||
gitTransport: MobileGitTransport
|
||||
operation: 'pull' | 'push'
|
||||
remote: Parameters<typeof runMobileGitTransportOperation>[0]['remote']
|
||||
setActiveOperation: (operation: MobileGitOperation | null) => void
|
||||
setFailure: (failure: MobileGitSyncFlowFailure | null) => void
|
||||
vault: MobileVaultMetadata
|
||||
}) {
|
||||
setFailure(null)
|
||||
setActiveOperation(operation)
|
||||
void runMobileGitTransportOperation({
|
||||
operation,
|
||||
remote,
|
||||
transport: gitTransport,
|
||||
vault,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.state === 'failed') {
|
||||
setFailure({ message: result.message, operation })
|
||||
}
|
||||
})
|
||||
.catch(() => setFailure({ message: 'Mobile Git sync failed.', operation }))
|
||||
.finally(() => setActiveOperation(null))
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export type MobileGitSyncPlan =
|
||||
state: 'ready'
|
||||
canPull: true
|
||||
canPush: boolean
|
||||
primaryAction: MobileGitOperation
|
||||
primaryAction: 'pull' | 'push'
|
||||
remote: MobileGitRemote
|
||||
}
|
||||
| {
|
||||
|
||||
52
apps/mobile/src/mobileGitTransport.test.ts
Normal file
52
apps/mobile/src/mobileGitTransport.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
createUnavailableMobileGitTransport,
|
||||
runMobileGitTransportOperation,
|
||||
type MobileGitTransport,
|
||||
} from './mobileGitTransport'
|
||||
import type { MobileGitRemote } from './mobileGitRemote'
|
||||
|
||||
describe('mobile git transport', () => {
|
||||
it('runs the requested transport operation', async () => {
|
||||
const calls: string[] = []
|
||||
const transport: MobileGitTransport = {
|
||||
pull: async () => {
|
||||
calls.push('pull')
|
||||
return { state: 'completed' }
|
||||
},
|
||||
push: async () => {
|
||||
calls.push('push')
|
||||
return { state: 'completed' }
|
||||
},
|
||||
}
|
||||
|
||||
await runMobileGitTransportOperation({
|
||||
operation: 'push',
|
||||
remote: remote(),
|
||||
transport,
|
||||
vault: { id: 'personal', name: 'Personal Journal' },
|
||||
})
|
||||
|
||||
expect(calls).toEqual(['push'])
|
||||
})
|
||||
|
||||
it('fails explicitly until a native git implementation is wired', async () => {
|
||||
await expect(createUnavailableMobileGitTransport().pull({
|
||||
remote: remote(),
|
||||
vault: { id: 'personal', name: 'Personal Journal' },
|
||||
})).resolves.toEqual({
|
||||
message: 'Mobile Git transport is not available yet.',
|
||||
state: 'failed',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function remote(): MobileGitRemote {
|
||||
return {
|
||||
authStrategy: 'githubOAuth',
|
||||
host: 'github.com',
|
||||
owner: 'refactoringhq',
|
||||
repository: 'tolaria',
|
||||
url: 'https://github.com/refactoringhq/tolaria.git',
|
||||
}
|
||||
}
|
||||
50
apps/mobile/src/mobileGitTransport.ts
Normal file
50
apps/mobile/src/mobileGitTransport.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { MobileGitRemote } from './mobileGitRemote'
|
||||
import type { MobileVaultMetadata } from './mobileVaultMetadata'
|
||||
import type { MobileGitTransportOperation } from './mobileGitPrimaryAction'
|
||||
|
||||
export type MobileGitTransportRequest = {
|
||||
remote: MobileGitRemote
|
||||
vault: MobileVaultMetadata
|
||||
}
|
||||
|
||||
export type MobileGitTransportResult =
|
||||
| {
|
||||
state: 'completed'
|
||||
}
|
||||
| {
|
||||
message: string
|
||||
state: 'failed'
|
||||
}
|
||||
|
||||
export type MobileGitTransport = {
|
||||
pull: (request: MobileGitTransportRequest) => Promise<MobileGitTransportResult>
|
||||
push: (request: MobileGitTransportRequest) => Promise<MobileGitTransportResult>
|
||||
}
|
||||
|
||||
export function createUnavailableMobileGitTransport(): MobileGitTransport {
|
||||
return {
|
||||
pull: () => unavailableTransportResult(),
|
||||
push: () => unavailableTransportResult(),
|
||||
}
|
||||
}
|
||||
|
||||
export function runMobileGitTransportOperation({
|
||||
operation,
|
||||
remote,
|
||||
transport,
|
||||
vault,
|
||||
}: {
|
||||
operation: MobileGitTransportOperation
|
||||
remote: MobileGitRemote
|
||||
transport: MobileGitTransport
|
||||
vault: MobileVaultMetadata
|
||||
}) {
|
||||
return transport[operation]({ remote, vault })
|
||||
}
|
||||
|
||||
function unavailableTransportResult(): Promise<MobileGitTransportResult> {
|
||||
return Promise.resolve({
|
||||
message: 'Mobile Git transport is not available yet.',
|
||||
state: 'failed',
|
||||
})
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import type { MobileGitCredentialStorage } from './mobileGitCredentialStorage'
|
||||
import { loadMobileGitCredentialStateForVault } from './mobileGitCredentialStateForVault'
|
||||
import type { MobileGitCredentialState } from './mobileGitSyncPlan'
|
||||
import type { MobileGitCredentialState, MobileGitOperation } from './mobileGitSyncPlan'
|
||||
import { createMobileGitSyncPlanForVault } from './mobileGitSyncRuntimePlan'
|
||||
import { authenticateMobileGitSyncPlan } from './mobileGitAuthentication'
|
||||
import { runMobileGitSyncFlowAction, type MobileGitSyncFlowFailure } from './mobileGitSyncFlowAction'
|
||||
import type { MobileGitTransport } from './mobileGitTransport'
|
||||
import type { MobileGitHubOAuthSession } from './mobileGitHubOAuthFlow'
|
||||
import type { MobileVaultMetadata } from './mobileVaultMetadata'
|
||||
|
||||
export function useMobileGitSyncFlow({
|
||||
createGitHubOAuthSession,
|
||||
credentialStorage,
|
||||
gitTransport,
|
||||
vault,
|
||||
}: {
|
||||
createGitHubOAuthSession: () => MobileGitHubOAuthSession
|
||||
credentialStorage: MobileGitCredentialStorage
|
||||
gitTransport: MobileGitTransport
|
||||
vault: MobileVaultMetadata
|
||||
}) {
|
||||
const [authFailureMessage, setAuthFailureMessage] = useState<string | null>(null)
|
||||
const [failure, setFailure] = useState<MobileGitSyncFlowFailure | null>(null)
|
||||
const [credentials, setCredentials] = useState<MobileGitCredentialState>({ state: 'missing' })
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false)
|
||||
const [activeOperation, setActiveOperation] = useState<MobileGitOperation | null>(null)
|
||||
const refreshCredentials = useCallback(() => {
|
||||
void loadMobileGitCredentialStateForVault({ credentialStorage, vault })
|
||||
.then(setCredentials)
|
||||
@@ -27,43 +30,30 @@ export function useMobileGitSyncFlow({
|
||||
const gitSyncPlan = useMemo(
|
||||
() => createMobileGitSyncPlanForVault({
|
||||
credentials,
|
||||
...(authFailureMessage ? { failure: { message: authFailureMessage, operation: 'clone' } } : {}),
|
||||
...(isAuthenticating ? { operation: 'clone' } : {}),
|
||||
...(failure ? { failure } : {}),
|
||||
...(activeOperation ? { operation: activeOperation } : {}),
|
||||
vault,
|
||||
}),
|
||||
[authFailureMessage, credentials, isAuthenticating, vault],
|
||||
[activeOperation, credentials, failure, vault],
|
||||
)
|
||||
const authenticate = useCallback(() => {
|
||||
if (isAuthenticating) {
|
||||
return
|
||||
}
|
||||
|
||||
setAuthFailureMessage(null)
|
||||
setIsAuthenticating(true)
|
||||
void authenticateMobileGitSyncPlan({
|
||||
const runPrimaryAction = useCallback(() => {
|
||||
runMobileGitSyncFlowAction({
|
||||
activeOperation,
|
||||
credentialStorage,
|
||||
createGitHubOAuthSession,
|
||||
now: () => new Date().toISOString(),
|
||||
plan: gitSyncPlan,
|
||||
gitSyncPlan,
|
||||
gitTransport,
|
||||
refreshCredentials,
|
||||
setActiveOperation,
|
||||
setFailure,
|
||||
vault,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.state === 'connected') {
|
||||
refreshCredentials()
|
||||
return
|
||||
}
|
||||
|
||||
if (result.state === 'failed') {
|
||||
setAuthFailureMessage(result.message)
|
||||
}
|
||||
})
|
||||
.catch(() => setAuthFailureMessage('GitHub authentication failed.'))
|
||||
.finally(() => setIsAuthenticating(false))
|
||||
}, [createGitHubOAuthSession, credentialStorage, gitSyncPlan, isAuthenticating, refreshCredentials])
|
||||
}, [activeOperation, createGitHubOAuthSession, credentialStorage, gitSyncPlan, gitTransport, refreshCredentials, vault])
|
||||
|
||||
useEffect(refreshCredentials, [refreshCredentials])
|
||||
|
||||
return {
|
||||
authenticate,
|
||||
gitSyncPlan,
|
||||
runPrimaryAction,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +97,14 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
|
||||
- Exposed the missing `EXPO_PUBLIC_GITHUB_OAUTH_CLIENT_ID` requirement inside the mobile remote setup prompt and split the client-id detector away from native AuthSession imports.
|
||||
- Separated the mobile Git remote setup prompt styles from note-creation prompt styles so vault-management UI can evolve without coupling to compose UI names.
|
||||
- Added a sidebar vault-management card that shows the active app-local vault, Git sync state, and an explicit Git remote action on iPad and compact sidebar surfaces.
|
||||
- Added the first mobile Git transport execution boundary behind the existing sync/auth plan, including explicit pull/push routing and a visible unavailable-transport failure until the native Git implementation lands.
|
||||
|
||||
## Next Action
|
||||
|
||||
Continue Phase 4 with editor durability:
|
||||
|
||||
1. Continue TenTap Markdown serialization coverage for any editor output observed in simulator QA.
|
||||
2. Start the mobile Git transport execution boundary behind the existing sync/auth plan.
|
||||
2. Start the native Git engine spike behind `MobileGitTransport`, preferably Rust/libgit2 unless Expo native-module constraints block it.
|
||||
3. Retry the iOS development-client build after installing an iOS 26.2 simulator runtime in Xcode.
|
||||
|
||||
## Verification Log
|
||||
@@ -366,6 +367,10 @@ Continue Phase 4 with editor durability:
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after adding the vault-management sidebar card.
|
||||
- CodeScene after adding the vault-management sidebar card: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/MobileVaultManagementCard.tsx`, `apps/mobile/src/mobileVaultManagementSummary.ts`, `apps/mobile/src/mobileVaultManagementSummary.test.ts`, and `apps/mobile/src/styles/vaultManagementStyles.ts` scored `10`; `apps/mobile/src/styles.ts` returned no scorable code and no findings.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after adding the vault-management sidebar card; Metro recovered from a cache deserialize warning by doing a full crawl.
|
||||
- `pnpm --filter @tolaria/mobile test -- src/mobileGitPrimaryAction.test.ts src/mobileGitTransport.test.ts src/mobileGitSyncFlowAction.test.ts src/mobileGitSyncRuntimePlan.test.ts src/mobileGitSyncStatus.test.ts` passed after adding the mobile Git transport execution boundary: 43 files / 143 tests.
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after adding the mobile Git transport execution boundary.
|
||||
- CodeScene after adding the mobile Git transport execution boundary: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/useMobileGitSyncFlow.ts`, `apps/mobile/src/mobileGitSyncPlan.ts`, `apps/mobile/src/mobileGitPrimaryAction.ts`, `apps/mobile/src/mobileGitPrimaryAction.test.ts`, `apps/mobile/src/mobileGitTransport.ts`, `apps/mobile/src/mobileGitTransport.test.ts`, `apps/mobile/src/mobileGitSyncFlowAction.ts`, and `apps/mobile/src/mobileGitSyncFlowAction.test.ts` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after adding the mobile Git transport execution boundary.
|
||||
|
||||
## Risks / Watch Items
|
||||
|
||||
|
||||
Reference in New Issue
Block a user