feat: wire mobile git auth action
This commit is contained in:
@@ -52,18 +52,21 @@ import { useMobileNoteDeleteFlow } from './useMobileNoteDeleteFlow'
|
||||
import { useMobileNotePropertiesFlow } from './useMobileNotePropertiesFlow'
|
||||
import { createNativeMobileAppStateStorage } from './mobileNativeAppStateStorage'
|
||||
import { createNativeMobileVaultMetadataStorage } from './mobileNativeVaultMetadataStorage'
|
||||
import { createMobileGitSyncPlanForVault } from './mobileGitSyncRuntimePlan'
|
||||
import { createNativeMobileGitCredentialStorage } from './mobileNativeGitCredentialStorage'
|
||||
import { createNativeMobileGitHubOAuthSessionFromEnvironment } from './mobileGitHubOAuthEnvironment'
|
||||
import type { MobileGitSyncPlan } from './mobileGitSyncPlan'
|
||||
import { defaultMobileVaultMetadata } from './mobileVaultMetadata'
|
||||
import type { MobileVaultRuntime } from './mobileVaultRuntime'
|
||||
import { useMobileVaultRuntimeLoader } from './useMobileVaultRuntimeLoader'
|
||||
import type { MobileNotePropertyPatch } from './mobileNoteProperties'
|
||||
import { useMobileGitSyncFlow } from './useMobileGitSyncFlow'
|
||||
|
||||
export function MobileApp() {
|
||||
const { width } = useWindowDimensions()
|
||||
const isTablet = width >= 820
|
||||
const showsProperties = width >= 1000
|
||||
const appStateStorage = useMemo(() => createNativeMobileAppStateStorage(), [])
|
||||
const gitCredentialStorage = useMemo(() => createNativeMobileGitCredentialStorage(), [])
|
||||
const vaultMetadataStorage = useMemo(() => createNativeMobileVaultMetadataStorage(), [])
|
||||
const [activeVaultMetadata, setActiveVaultMetadata] = useState(defaultMobileVaultMetadata)
|
||||
const [availableNotes, setAvailableNotes] = useState(fallbackNotes)
|
||||
@@ -74,10 +77,11 @@ export function MobileApp() {
|
||||
[availableNotes, compactNavigation.selectedNoteId],
|
||||
)
|
||||
const selectedSaveState = saveStateByNoteId[selectedNote.id] ?? idleMobileEditorSaveState
|
||||
const gitSyncPlan = useMemo(
|
||||
() => createMobileGitSyncPlanForVault({ vault: activeVaultMetadata }),
|
||||
[activeVaultMetadata],
|
||||
)
|
||||
const gitSyncFlow = useMobileGitSyncFlow({
|
||||
createGitHubOAuthSession: createNativeMobileGitHubOAuthSessionFromEnvironment,
|
||||
credentialStorage: gitCredentialStorage,
|
||||
vault: activeVaultMetadata,
|
||||
})
|
||||
const autosaveQueue = useMemo(
|
||||
() =>
|
||||
createMobileAutosaveQueue({
|
||||
@@ -147,7 +151,7 @@ export function MobileApp() {
|
||||
<View style={styles.tabletShell}>
|
||||
<SidebarPanel />
|
||||
<NoteListPanel
|
||||
gitSyncPlan={gitSyncPlan}
|
||||
gitSyncPlan={gitSyncFlow.gitSyncPlan}
|
||||
notes={availableNotes}
|
||||
selectedNoteId={compactNavigation.selectedNoteId}
|
||||
createNoteFailed={createFlow.failed}
|
||||
@@ -158,6 +162,7 @@ export function MobileApp() {
|
||||
onCancelCreateNote={createFlow.cancel}
|
||||
onChangeCreateNoteTitle={createFlow.setTitle}
|
||||
onOpenCreateNote={createFlow.open}
|
||||
onGitSyncAction={gitSyncFlow.authenticate}
|
||||
onRetryRuntimeLoad={runtimeLoader.retry}
|
||||
onSubmitCreateNote={createFlow.submit}
|
||||
onSelectNote={selectNote}
|
||||
@@ -181,7 +186,7 @@ export function MobileApp() {
|
||||
<CompactShell
|
||||
activePanel={compactNavigation.panel}
|
||||
note={selectedNote}
|
||||
gitSyncPlan={gitSyncPlan}
|
||||
gitSyncPlan={gitSyncFlow.gitSyncPlan}
|
||||
notes={availableNotes}
|
||||
saveState={selectedSaveState}
|
||||
selectedNoteId={compactNavigation.selectedNoteId}
|
||||
@@ -196,6 +201,7 @@ export function MobileApp() {
|
||||
onCancelCreateNote={createFlow.cancel}
|
||||
onChangeCreateNoteTitle={createFlow.setTitle}
|
||||
onOpenCreateNote={createFlow.open}
|
||||
onGitSyncAction={gitSyncFlow.authenticate}
|
||||
onRetryRuntimeLoad={runtimeLoader.retry}
|
||||
onSubmitCreateNote={createFlow.submit}
|
||||
onSelectNote={selectNote}
|
||||
@@ -226,6 +232,7 @@ function CompactShell({
|
||||
onCancelCreateNote,
|
||||
onChangeCreateNoteTitle,
|
||||
onOpenCreateNote,
|
||||
onGitSyncAction,
|
||||
onChangeProperties,
|
||||
onRetryRuntimeLoad,
|
||||
onSubmitCreateNote,
|
||||
@@ -250,6 +257,7 @@ function CompactShell({
|
||||
onCancelCreateNote: () => void
|
||||
onChangeCreateNoteTitle: (title: string) => void
|
||||
onOpenCreateNote: () => void
|
||||
onGitSyncAction: () => void
|
||||
onChangeProperties: (patch: MobileNotePropertyPatch) => void
|
||||
onRetryRuntimeLoad: () => void
|
||||
onSubmitCreateNote: () => void
|
||||
@@ -308,6 +316,7 @@ function CompactShell({
|
||||
runtimeLoadFailed={runtimeLoadFailed}
|
||||
onCancelCreateNote={onCancelCreateNote}
|
||||
onChangeCreateNoteTitle={onChangeCreateNoteTitle}
|
||||
onGitSyncAction={onGitSyncAction}
|
||||
onOpenCreateNote={onOpenCreateNote}
|
||||
onOpenSidebar={() => onNavigate({ type: 'openSidebar' })}
|
||||
onRetryRuntimeLoad={onRetryRuntimeLoad}
|
||||
@@ -361,6 +370,7 @@ function NoteListPanel({
|
||||
runtimeLoadFailed,
|
||||
onCancelCreateNote,
|
||||
onChangeCreateNoteTitle,
|
||||
onGitSyncAction,
|
||||
onOpenCreateNote,
|
||||
onOpenSidebar,
|
||||
onRetryRuntimeLoad,
|
||||
@@ -377,6 +387,7 @@ function NoteListPanel({
|
||||
runtimeLoadFailed: boolean
|
||||
onCancelCreateNote: () => void
|
||||
onChangeCreateNoteTitle: (title: string) => void
|
||||
onGitSyncAction: () => void
|
||||
onOpenCreateNote: () => void
|
||||
onOpenSidebar?: () => void
|
||||
onRetryRuntimeLoad: () => void
|
||||
@@ -392,7 +403,7 @@ function NoteListPanel({
|
||||
<View style={styles.toolbarSpacer} />
|
||||
<IconButton icon={<MagnifyingGlass size={23} color={colors.textSoft} />} />
|
||||
</Toolbar>
|
||||
<MobileGitSyncStatusCard plan={gitSyncPlan} />
|
||||
<MobileGitSyncStatusCard plan={gitSyncPlan} onPrimaryAction={onGitSyncAction} />
|
||||
{runtimeLoadFailed ? <VaultLoadErrorNotice onRetry={onRetryRuntimeLoad} /> : null}
|
||||
<FlatList
|
||||
data={notes}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { GitBranch, WarningCircle } from 'phosphor-react-native'
|
||||
import { Text, View } from 'react-native'
|
||||
import { Pressable, Text, View } from 'react-native'
|
||||
import { mobileGitSyncStatusView, type MobileGitSyncStatusTone } from './mobileGitSyncStatus'
|
||||
import type { MobileGitSyncPlan } from './mobileGitSyncPlan'
|
||||
import { styles } from './styles'
|
||||
import { colors } from './theme'
|
||||
|
||||
export function MobileGitSyncStatusCard({ plan }: { plan: MobileGitSyncPlan }) {
|
||||
export function MobileGitSyncStatusCard({
|
||||
onPrimaryAction,
|
||||
plan,
|
||||
}: {
|
||||
onPrimaryAction?: () => void
|
||||
plan: MobileGitSyncPlan
|
||||
}) {
|
||||
const status = mobileGitSyncStatusView(plan)
|
||||
if (!status) {
|
||||
return null
|
||||
@@ -20,7 +26,15 @@ export function MobileGitSyncStatusCard({ plan }: { plan: MobileGitSyncPlan }) {
|
||||
<Text style={styles.gitSyncStatusLabel}>{status.label}</Text>
|
||||
<Text style={styles.gitSyncStatusDetail}>{status.detail}</Text>
|
||||
</View>
|
||||
{status.actionLabel ? <Text style={styles.gitSyncStatusAction}>{status.actionLabel}</Text> : null}
|
||||
{status.actionLabel ? (
|
||||
<Pressable
|
||||
accessibilityLabel={status.actionLabel}
|
||||
onPress={onPrimaryAction}
|
||||
style={({ pressed }) => pressed ? styles.pressed : null}
|
||||
>
|
||||
<Text style={styles.gitSyncStatusAction}>{status.actionLabel}</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
85
apps/mobile/src/mobileGitAuthentication.test.ts
Normal file
85
apps/mobile/src/mobileGitAuthentication.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { MobileGitCredentialStorage } from './mobileGitCredentialStorage'
|
||||
import { authenticateMobileGitSyncPlan } from './mobileGitAuthentication'
|
||||
|
||||
describe('authenticateMobileGitSyncPlan', () => {
|
||||
it('ignores plans that do not need authentication', async () => {
|
||||
await expect(authenticateMobileGitSyncPlan({
|
||||
credentialStorage: noopCredentialStorage(),
|
||||
createGitHubOAuthSession: failingSession,
|
||||
now: () => '2026-05-05T12:00:00.000Z',
|
||||
plan: { primaryAction: null, state: 'localOnly' },
|
||||
})).resolves.toEqual({ state: 'ignored' })
|
||||
})
|
||||
|
||||
it('connects GitHub auth-required plans through the OAuth session', async () => {
|
||||
const credentialStorage = memoryCredentialStorage()
|
||||
|
||||
await expect(authenticateMobileGitSyncPlan({
|
||||
credentialStorage,
|
||||
createGitHubOAuthSession: () => ({
|
||||
authorize: async () => ({
|
||||
state: 'succeeded',
|
||||
token: { accessToken: 'token', tokenType: 'bearer' },
|
||||
}),
|
||||
}),
|
||||
now: () => '2026-05-05T12:00:00.000Z',
|
||||
plan: {
|
||||
authStrategy: 'githubOAuth',
|
||||
host: 'github.com',
|
||||
primaryAction: 'authenticate',
|
||||
state: 'authRequired',
|
||||
},
|
||||
})).resolves.toEqual({ state: 'connected' })
|
||||
|
||||
await expect(credentialStorage.loadState({ host: 'github.com', strategy: 'githubOAuth' }))
|
||||
.resolves.toEqual({ state: 'available' })
|
||||
})
|
||||
|
||||
it('fails unsupported SSH authentication without starting OAuth', async () => {
|
||||
await expect(authenticateMobileGitSyncPlan({
|
||||
credentialStorage: noopCredentialStorage(),
|
||||
createGitHubOAuthSession: failingSession,
|
||||
now: () => '2026-05-05T12:00:00.000Z',
|
||||
plan: {
|
||||
authStrategy: 'sshKey',
|
||||
host: 'git.example.com',
|
||||
primaryAction: 'authenticate',
|
||||
state: 'authRequired',
|
||||
},
|
||||
})).resolves.toEqual({
|
||||
message: 'SSH credential setup is not available yet.',
|
||||
state: 'failed',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function memoryCredentialStorage(): MobileGitCredentialStorage {
|
||||
let isAvailable = false
|
||||
|
||||
return {
|
||||
loadState: async () => isAvailable ? { state: 'available' } : { state: 'missing' },
|
||||
remove: async () => {
|
||||
isAvailable = false
|
||||
},
|
||||
saveRecord: async () => {
|
||||
isAvailable = true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function noopCredentialStorage(): MobileGitCredentialStorage {
|
||||
return {
|
||||
loadState: async () => ({ state: 'missing' }),
|
||||
remove: async () => {},
|
||||
saveRecord: async () => {},
|
||||
}
|
||||
}
|
||||
|
||||
function failingSession() {
|
||||
return {
|
||||
authorize: async () => {
|
||||
throw new Error('should not start OAuth')
|
||||
},
|
||||
}
|
||||
}
|
||||
55
apps/mobile/src/mobileGitAuthentication.ts
Normal file
55
apps/mobile/src/mobileGitAuthentication.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { MobileGitCredentialStorage } from './mobileGitCredentialStorage'
|
||||
import type { MobileGitSyncPlan } from './mobileGitSyncPlan'
|
||||
import { connectMobileGitHubOAuth, type MobileGitHubOAuthSession } from './mobileGitHubOAuthFlow'
|
||||
|
||||
export type MobileGitAuthenticationResult =
|
||||
| {
|
||||
state: 'connected'
|
||||
}
|
||||
| {
|
||||
state: 'cancelled'
|
||||
}
|
||||
| {
|
||||
message: string
|
||||
state: 'failed'
|
||||
}
|
||||
| {
|
||||
state: 'ignored'
|
||||
}
|
||||
|
||||
export async function authenticateMobileGitSyncPlan({
|
||||
credentialStorage,
|
||||
createGitHubOAuthSession,
|
||||
now,
|
||||
plan,
|
||||
}: {
|
||||
credentialStorage: MobileGitCredentialStorage
|
||||
createGitHubOAuthSession: () => MobileGitHubOAuthSession
|
||||
now: () => string
|
||||
plan: MobileGitSyncPlan
|
||||
}): Promise<MobileGitAuthenticationResult> {
|
||||
if (!canAuthenticate(plan)) {
|
||||
return { state: 'ignored' }
|
||||
}
|
||||
|
||||
if (authStrategy(plan) !== 'githubOAuth') {
|
||||
return {
|
||||
message: 'SSH credential setup is not available yet.',
|
||||
state: 'failed',
|
||||
}
|
||||
}
|
||||
|
||||
return connectMobileGitHubOAuth({
|
||||
credentialStorage,
|
||||
now,
|
||||
session: createGitHubOAuthSession(),
|
||||
})
|
||||
}
|
||||
|
||||
function canAuthenticate(plan: MobileGitSyncPlan) {
|
||||
return plan.state === 'authRequired' || plan.state === 'failed'
|
||||
}
|
||||
|
||||
function authStrategy(plan: Extract<MobileGitSyncPlan, { state: 'authRequired' | 'failed' }>) {
|
||||
return plan.state === 'authRequired' ? plan.authStrategy : plan.remote.authStrategy
|
||||
}
|
||||
58
apps/mobile/src/mobileGitCredentialStateForVault.test.ts
Normal file
58
apps/mobile/src/mobileGitCredentialStateForVault.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
createMobileGitCredentialRecord,
|
||||
type MobileGitCredentialStorage,
|
||||
} from './mobileGitCredentialStorage'
|
||||
import { loadMobileGitCredentialStateForVault } from './mobileGitCredentialStateForVault'
|
||||
|
||||
describe('loadMobileGitCredentialStateForVault', () => {
|
||||
it('keeps local-only vaults credential-missing without hitting secure storage', async () => {
|
||||
await expect(loadMobileGitCredentialStateForVault({
|
||||
credentialStorage: failingCredentialStorage(),
|
||||
vault: { id: 'personal', name: 'Personal Journal' },
|
||||
})).resolves.toEqual({ state: 'missing' })
|
||||
})
|
||||
|
||||
it('loads credential state for a remote-backed vault auth requirement', async () => {
|
||||
const credentialStorage = memoryCredentialStorage()
|
||||
await credentialStorage.saveRecord(createMobileGitCredentialRecord({
|
||||
requirement: { host: 'github.com', strategy: 'githubOAuth' },
|
||||
storedAt: '2026-05-05T12:00:00.000Z',
|
||||
}))
|
||||
|
||||
await expect(loadMobileGitCredentialStateForVault({
|
||||
credentialStorage,
|
||||
vault: {
|
||||
id: 'tolaria',
|
||||
name: 'Tolaria',
|
||||
remoteUrl: 'https://github.com/refactoringhq/tolaria.git',
|
||||
},
|
||||
})).resolves.toEqual({ state: 'available' })
|
||||
})
|
||||
})
|
||||
|
||||
function memoryCredentialStorage(): MobileGitCredentialStorage {
|
||||
const records = new Map<string, ReturnType<typeof createMobileGitCredentialRecord>>()
|
||||
|
||||
return {
|
||||
loadState: async (requirement) => records.has(`${requirement.strategy}:${requirement.host}`)
|
||||
? { state: 'available' }
|
||||
: { state: 'missing' },
|
||||
remove: async (requirement) => {
|
||||
records.delete(`${requirement.strategy}:${requirement.host}`)
|
||||
},
|
||||
saveRecord: async (record) => {
|
||||
records.set(`${record.strategy}:${record.host}`, record)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function failingCredentialStorage(): MobileGitCredentialStorage {
|
||||
return {
|
||||
loadState: async () => {
|
||||
throw new Error('should not load credentials')
|
||||
},
|
||||
remove: async () => {},
|
||||
saveRecord: async () => {},
|
||||
}
|
||||
}
|
||||
19
apps/mobile/src/mobileGitCredentialStateForVault.ts
Normal file
19
apps/mobile/src/mobileGitCredentialStateForVault.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { MobileGitCredentialStorage } from './mobileGitCredentialStorage'
|
||||
import type { MobileGitCredentialState } from './mobileGitSyncPlan'
|
||||
import { createMobileVaultConfig } from './mobileVaultConfig'
|
||||
import type { MobileVaultMetadata } from './mobileVaultMetadata'
|
||||
|
||||
export async function loadMobileGitCredentialStateForVault({
|
||||
credentialStorage,
|
||||
vault,
|
||||
}: {
|
||||
credentialStorage: MobileGitCredentialStorage
|
||||
vault: MobileVaultMetadata
|
||||
}): Promise<MobileGitCredentialState> {
|
||||
const result = createMobileVaultConfig(vault)
|
||||
if (!result.ok || result.config.sync.state === 'localOnly') {
|
||||
return { state: 'missing' }
|
||||
}
|
||||
|
||||
return credentialStorage.loadState(result.config.sync.authRequirement)
|
||||
}
|
||||
18
apps/mobile/src/mobileGitHubOAuthEnvironment.ts
Normal file
18
apps/mobile/src/mobileGitHubOAuthEnvironment.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createNativeMobileGitHubOAuthSession } from './mobileNativeGitHubOAuthSession'
|
||||
import type { MobileGitHubOAuthSession } from './mobileGitHubOAuthFlow'
|
||||
|
||||
declare const process: { env?: Record<string, string | undefined> } | undefined
|
||||
|
||||
export function createNativeMobileGitHubOAuthSessionFromEnvironment(): MobileGitHubOAuthSession {
|
||||
const clientId = process?.env?.EXPO_PUBLIC_GITHUB_OAUTH_CLIENT_ID?.trim() ?? ''
|
||||
if (!clientId) {
|
||||
return {
|
||||
authorize: async () => ({
|
||||
message: 'GitHub OAuth client ID is not configured.',
|
||||
state: 'failed',
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return createNativeMobileGitHubOAuthSession({ clientId })
|
||||
}
|
||||
@@ -1,12 +1,21 @@
|
||||
import { createMobileGitSyncPlan, type MobileGitCredentialState, type MobileGitSyncPlan } from './mobileGitSyncPlan'
|
||||
import {
|
||||
createMobileGitSyncPlan,
|
||||
type MobileGitCredentialState,
|
||||
type MobileGitOperation,
|
||||
type MobileGitSyncPlan,
|
||||
} from './mobileGitSyncPlan'
|
||||
import { createMobileVaultConfig } from './mobileVaultConfig'
|
||||
import type { MobileVaultMetadata } from './mobileVaultMetadata'
|
||||
|
||||
export function createMobileGitSyncPlanForVault({
|
||||
credentials = { state: 'missing' },
|
||||
failure,
|
||||
operation,
|
||||
vault,
|
||||
}: {
|
||||
credentials?: MobileGitCredentialState
|
||||
failure?: { message: string; operation: MobileGitOperation }
|
||||
operation?: MobileGitOperation
|
||||
vault: MobileVaultMetadata
|
||||
}): MobileGitSyncPlan {
|
||||
const result = createMobileVaultConfig(vault)
|
||||
@@ -16,6 +25,8 @@ export function createMobileGitSyncPlanForVault({
|
||||
|
||||
return createMobileGitSyncPlan({
|
||||
credentials,
|
||||
failure,
|
||||
operation,
|
||||
sync: result.config.sync,
|
||||
})
|
||||
}
|
||||
|
||||
69
apps/mobile/src/useMobileGitSyncFlow.ts
Normal file
69
apps/mobile/src/useMobileGitSyncFlow.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import type { MobileGitCredentialStorage } from './mobileGitCredentialStorage'
|
||||
import { loadMobileGitCredentialStateForVault } from './mobileGitCredentialStateForVault'
|
||||
import type { MobileGitCredentialState } from './mobileGitSyncPlan'
|
||||
import { createMobileGitSyncPlanForVault } from './mobileGitSyncRuntimePlan'
|
||||
import { authenticateMobileGitSyncPlan } from './mobileGitAuthentication'
|
||||
import type { MobileGitHubOAuthSession } from './mobileGitHubOAuthFlow'
|
||||
import type { MobileVaultMetadata } from './mobileVaultMetadata'
|
||||
|
||||
export function useMobileGitSyncFlow({
|
||||
createGitHubOAuthSession,
|
||||
credentialStorage,
|
||||
vault,
|
||||
}: {
|
||||
createGitHubOAuthSession: () => MobileGitHubOAuthSession
|
||||
credentialStorage: MobileGitCredentialStorage
|
||||
vault: MobileVaultMetadata
|
||||
}) {
|
||||
const [authFailureMessage, setAuthFailureMessage] = useState<string | null>(null)
|
||||
const [credentials, setCredentials] = useState<MobileGitCredentialState>({ state: 'missing' })
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false)
|
||||
const refreshCredentials = useCallback(() => {
|
||||
void loadMobileGitCredentialStateForVault({ credentialStorage, vault })
|
||||
.then(setCredentials)
|
||||
.catch(() => setCredentials({ state: 'missing' }))
|
||||
}, [credentialStorage, vault])
|
||||
const gitSyncPlan = useMemo(
|
||||
() => createMobileGitSyncPlanForVault({
|
||||
credentials,
|
||||
...(authFailureMessage ? { failure: { message: authFailureMessage, operation: 'clone' } } : {}),
|
||||
...(isAuthenticating ? { operation: 'clone' } : {}),
|
||||
vault,
|
||||
}),
|
||||
[authFailureMessage, credentials, isAuthenticating, vault],
|
||||
)
|
||||
const authenticate = useCallback(() => {
|
||||
if (isAuthenticating) {
|
||||
return
|
||||
}
|
||||
|
||||
setAuthFailureMessage(null)
|
||||
setIsAuthenticating(true)
|
||||
void authenticateMobileGitSyncPlan({
|
||||
credentialStorage,
|
||||
createGitHubOAuthSession,
|
||||
now: () => new Date().toISOString(),
|
||||
plan: gitSyncPlan,
|
||||
})
|
||||
.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])
|
||||
|
||||
useEffect(refreshCredentials, [refreshCredentials])
|
||||
|
||||
return {
|
||||
authenticate,
|
||||
gitSyncPlan,
|
||||
}
|
||||
}
|
||||
@@ -92,12 +92,13 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
|
||||
- Created [ADR-0113](./adr/0113-expo-secure-store-for-mobile-git-credentials.md) for the mobile secure credential storage dependency.
|
||||
- Added the mobile GitHub OAuth session boundary with Expo AuthSession/WebBrowser, PKCE request shaping, code exchange, SecureStore handoff, and the `tolaria://oauth/github` redirect scheme.
|
||||
- Created [ADR-0114](./adr/0114-expo-auth-session-for-mobile-github-oauth.md) for the mobile GitHub OAuth native dependency and redirect scheme.
|
||||
- Wired remote-backed mobile sync status actions to credential loading and the GitHub OAuth flow, including visible syncing/failed status states when Connect is tapped.
|
||||
|
||||
## Next Action
|
||||
|
||||
Continue Phase 4 with editor durability:
|
||||
|
||||
1. Wire remote-backed vault status actions to the GitHub OAuth flow so tapping Connect can populate credential state, still behind model-only Git operations until the native git adapter is selected.
|
||||
1. Add a mobile vault-management path for entering remote URLs and exposing the configured GitHub OAuth client ID requirement during setup.
|
||||
2. Continue TenTap Markdown serialization coverage for any editor output observed in simulator QA.
|
||||
3. Retry the iOS development-client build after installing an iOS 26.2 simulator runtime in Xcode.
|
||||
|
||||
@@ -341,6 +342,10 @@ Continue Phase 4 with editor durability:
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after mobile GitHub OAuth boundary.
|
||||
- CodeScene after mobile GitHub OAuth boundary: `apps/mobile/src/mobileGitCredentialStorage.ts`, `apps/mobile/src/mobileGitCredentialStorage.test.ts`, `apps/mobile/src/mobileGitHubOAuth.ts`, `apps/mobile/src/mobileGitHubOAuth.test.ts`, `apps/mobile/src/mobileGitHubOAuthFlow.ts`, `apps/mobile/src/mobileGitHubOAuthFlow.test.ts`, and `apps/mobile/src/mobileNativeGitHubOAuthSession.ts` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after mobile GitHub OAuth boundary.
|
||||
- `pnpm --filter @tolaria/mobile test -- src/mobileGitAuthentication.test.ts src/mobileGitCredentialStateForVault.test.ts src/mobileGitSyncRuntimePlan.test.ts src/mobileGitHubOAuthFlow.test.ts src/mobileGitHubOAuth.test.ts` passed after wiring Connect to mobile auth: 37 files / 126 tests.
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after wiring Connect to mobile auth.
|
||||
- CodeScene after wiring Connect to mobile auth: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/MobileGitSyncStatusCard.tsx`, `apps/mobile/src/useMobileGitSyncFlow.ts`, `apps/mobile/src/mobileGitAuthentication.ts`, `apps/mobile/src/mobileGitAuthentication.test.ts`, `apps/mobile/src/mobileGitCredentialStateForVault.ts`, `apps/mobile/src/mobileGitCredentialStateForVault.test.ts`, and `apps/mobile/src/mobileGitHubOAuthEnvironment.ts` scored `10`; `apps/mobile/src/mobileGitSyncRuntimePlan.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 wiring Connect to mobile auth.
|
||||
|
||||
## Risks / Watch Items
|
||||
|
||||
|
||||
@@ -732,6 +732,7 @@ Rules:
|
||||
- Store tokens and private keys only in Keychain / Android Keystore.
|
||||
- Keep the JavaScript-facing credential contract at `available` / `missing` plus provider metadata; raw tokens and SSH material stay behind the secure storage and native Git credential callback boundary.
|
||||
- Use the `tolaria://oauth/github` redirect scheme for development builds; production redirect registration must match the final app scheme/bundle identity.
|
||||
- Read the OAuth App client ID from `EXPO_PUBLIC_GITHUB_OAUTH_CLIENT_ID`; if it is absent, the Connect action must fail visibly and avoid writing placeholder credentials.
|
||||
- Do not persist credentials inside Git remote URLs.
|
||||
- Use credential callbacks at operation time.
|
||||
- Redact credentials from logs, analytics, crash reports, and support bundles.
|
||||
|
||||
Reference in New Issue
Block a user