From 6eb408d5fac5ed7adc3626d082169ff84da8f2d5 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 14 Sep 2026 11:20:43 -0500 Subject: [PATCH 1/4] fix: adopt Cloud 2.50.0 fixes and probe incoming changes --- .zennotes-commit | 2 +- src/bridge/mobile-bridge.ts | 5 ++ .../mobile-cloud-sync.integration.test.ts | 47 +++++++++++++++++-- src/bridge/mobile-cloud-sync.ts | 15 ++++++ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/.zennotes-commit b/.zennotes-commit index 261c8c9..3119a4f 100644 --- a/.zennotes-commit +++ b/.zennotes-commit @@ -1 +1 @@ -431907dfb63a59192ff414839673446564ee737e +104416f560bb9e709620cabe9e1f225306f7a4ea diff --git a/src/bridge/mobile-bridge.ts b/src/bridge/mobile-bridge.ts index 55dd987..87a805d 100644 --- a/src/bridge/mobile-bridge.ts +++ b/src/bridge/mobile-bridge.ts @@ -104,6 +104,7 @@ import { restoreMobileCloudBackup, restoreMobileCloudBackupNote, syncMobileCloudVault, + hasMobileCloudVaultChanges, updateMobileCloudBackupSchedule, unlinkMobileCloudVault, deleteMobileCloudVault, @@ -845,6 +846,10 @@ export const mobileBridge: ZenBridge = { unlinkCloudVault: () => unlinkMobileCloudVault(activeMobileVault()), deleteCloudVault: () => deleteMobileCloudVault(activeMobileVault()), syncCloudVault: () => syncMobileCloudVault(activeMobileVault()), + hasCloudVaultChanges: () => { + const vault = activeVault() + return vault instanceof MobileVault ? hasMobileCloudVaultChanges(vault) : Promise.resolve(false) + }, getCloudBootstrapConflict: (conflict) => getMobileCloudBootstrapConflict(activeMobileVault(), conflict), resolveCloudBootstrapConflict: (resolution) => diff --git a/src/bridge/mobile-cloud-sync.integration.test.ts b/src/bridge/mobile-cloud-sync.integration.test.ts index e481790..ee50808 100644 --- a/src/bridge/mobile-cloud-sync.integration.test.ts +++ b/src/bridge/mobile-cloud-sync.integration.test.ts @@ -27,6 +27,8 @@ async function fixture(initial: Record = { 'note.md': 'Original' const reads: string[] = [] const refreshes: Record[] = [] const uploaded: CloudSyncMutation[] = [] + const manifestRequests: unknown[] = [] + let accountStatus = { state: 'connected', account: { base_url: 'https://sync.example.test' } } let cursor = 0 let clock = 1000 let failWritePath: string | null = null @@ -54,7 +56,10 @@ async function fixture(initial: Record = { 'note.md': 'Original' } const remote = { listVaults: async () => ({ data: [{ id: 'vault-1', name: 'Test vault' }] }), - manifest: async () => ({ data: [...remoteItems.values()], cursor, next_page: null }), + manifest: async (_vaultId: string, options?: unknown) => { + manifestRequests.push(options) + return { data: [...remoteItems.values()], cursor, next_page: null } + }, changes: async (_vaultId: string, after: number) => { beforeChanges?.() return { data: feed.filter((change) => change.sequence > after), cursor, has_more: false } @@ -164,15 +169,14 @@ async function fixture(initial: Record = { 'note.md': 'Original' './mobile-cloud-auth': { authenticatedCredential: async () => ({ base_url: 'https://sync.example.test', token: 'test-only' }), authenticatedClient: async () => remote, - getMobileCloudAccountStatus: async () => ({ - state: 'connected', account: { base_url: 'https://sync.example.test' } - }) + getMobileCloudAccountStatus: async () => accountStatus } }) await api.linkMobileCloudVault(vault, 'vault-1') const stateKey = () => [...persisted.keys()].find((path) => path.includes('/states/')) return { - api, vault, files, reads, uploaded, refreshes, remoteText, put, + api, vault, files, reads, uploaded, refreshes, remoteText, put, manifestRequests, + setAccountStatus: (value: typeof accountStatus) => { accountStatus = value }, sync: () => api.syncMobileCloudVault(vault), setFailWrite: (path: string | null) => { failWritePath = path }, setBeforeChanges: (callback: typeof beforeChanges) => { beforeChanges = callback }, @@ -191,6 +195,39 @@ async function fixture(initial: Record = { 'note.md': 'Original' } describe('mobile Cloud adapter wiring', () => { + it('detects remote changes from a one-item metadata manifest without scanning local files', async () => { + const h = await fixture() + assert.equal(await h.api.hasMobileCloudVaultChanges(h.vault), true) + assert.deepEqual(h.manifestRequests, []) + await h.sync() + h.manifestRequests.length = 0 + h.reads.length = 0 + h.refreshes.length = 0 + assert.equal(await h.api.hasMobileCloudVaultChanges(h.vault), false) + h.remoteText('note.md', 'Incoming edit') + assert.equal(await h.api.hasMobileCloudVaultChanges(h.vault), true) + assert.deepEqual(h.manifestRequests, [ + { includeContent: false, perPage: 1 }, { includeContent: false, perPage: 1 } + ]) + assert.deepEqual(h.reads, []) + assert.deepEqual(h.refreshes, []) + await h.sync() + assert.equal(await h.api.hasMobileCloudVaultChanges(h.vault), false) + }) + + it('does not probe an unlinked, disconnected, or different-origin account', async () => { + const h = await fixture() + await h.sync() + h.manifestRequests.length = 0 + h.setAccountStatus({ state: 'disconnected', account: { base_url: 'https://sync.example.test' } }) + assert.equal(await h.api.hasMobileCloudVaultChanges(h.vault), false) + h.setAccountStatus({ state: 'connected', account: { base_url: 'https://another.example.test' } }) + assert.equal(await h.api.hasMobileCloudVaultChanges(h.vault), false) + await h.api.unlinkMobileCloudVault(h.vault) + assert.equal(await h.api.hasMobileCloudVaultChanges(h.vault), false) + assert.deepEqual(h.manifestRequests, []) + }) + it('does not rescan the vault or reread acknowledged bytes during a no-op sync', async () => { const h = await fixture() await h.sync() diff --git a/src/bridge/mobile-cloud-sync.ts b/src/bridge/mobile-cloud-sync.ts index 3854d96..d2a28e3 100644 --- a/src/bridge/mobile-cloud-sync.ts +++ b/src/bridge/mobile-cloud-sync.ts @@ -103,6 +103,21 @@ export async function deleteMobileCloudVault(vault: MobileVault): Promise await service.deleteLinkedVault(hostVault(vault)) } +/** Check the server cursor without scanning the vault or downloading attachment bytes. */ +export async function hasMobileCloudVaultChanges(vault: MobileVault): Promise { + const link = await getMobileCloudVaultLink(vault) + if (!link) return false + const status = await getMobileCloudAccountStatus() + if (status.state !== 'connected' || status.account?.base_url !== link.base_url) return false + const value = await persistence.loadState(vault.fs.rootPath, link.base_url, link.vault_id) + const state = value as Partial | null + if (!state || state.version !== 1 || state.vault_id !== link.vault_id || + typeof state.cursor !== 'number' || !Number.isInteger(state.cursor) || state.cursor < 0) return true + const client = await authenticatedClient() + const manifest = await client.manifest(link.vault_id, { includeContent: false, perPage: 1 }) + return manifest.cursor !== state.cursor +} + export async function syncMobileCloudVault(vault: MobileVault): Promise { return service.sync(hostVault(vault, true)) } From d1c6ce0dfa7f96e3b0dd245a2bd9ec5a287d0122 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 14 Sep 2026 11:20:43 -0500 Subject: [PATCH 2/4] fix: prevent native heap exhaustion during Cloud downloads --- capacitor.config.ts | 5 +- src/bridge/cloud-sync-client.test.ts | 93 ++++++++++++++++++++++++++++ src/bridge/cloud-sync-client.ts | 13 ++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/bridge/cloud-sync-client.test.ts diff --git a/capacitor.config.ts b/capacitor.config.ts index c908fd6..6fc6215 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -6,7 +6,10 @@ const config: CapacitorConfig = { appName: 'ZenNotes', webDir: 'dist', android: { - backgroundColor: '#1d2021' + backgroundColor: '#1d2021', + // Native debug logging duplicates complete plugin payloads, including + // attachment base64 and credentials, and can exhaust Android's heap. + loggingBehavior: 'none' }, plugins: { Keyboard: { diff --git a/src/bridge/cloud-sync-client.test.ts b/src/bridge/cloud-sync-client.test.ts new file mode 100644 index 0000000..2e9df0b --- /dev/null +++ b/src/bridge/cloud-sync-client.test.ts @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict' +import { it } from 'node:test' +import { loadMobileModule } from '../../tooling/load-mobile-module.ts' + +it('downloads every manifest and change page without grouping large attachments in one native response', async () => { + const requests: URL[] = [] + const items = [1, 2, 3].map((id) => ({ + item_id: `asset-${id}`, path: `assets/${id}.jpg`, kind: 'binary', revision: 1, + sha256: `hash-${id}`, byte_length: 8_700_000, media_type: 'image/jpeg', + content: { + encoding: 'base64', data: `fixture-${id}`, sha256: `hash-${id}`, + byte_length: 8_700_000, media_type: 'image/jpeg' + } + })) + let feed: any[] = [] + const { createCloudSyncClient, CloudSyncCoordinator } = await loadMobileModule([ + './src/bridge/cloud-sync-client.ts', '@zennotes/shared-domain/cloud-sync-coordinator' + ], { + '@capacitor/core': { + registerPlugin: () => ({}), + CapacitorHttp: { + request: async ({ url }: { url: string }) => { + const request = new URL(url) + requests.push(request) + if (request.pathname.endsWith('/manifest')) { + const page = Number(request.searchParams.get('page') ?? 1) + const size = Number(request.searchParams.get('per_page') ?? 100) + const data = items.slice((page - 1) * size, page * size) + assert.ok(data.length <= 1, 'content responses must fit one attachment through the native bridge') + return { status: 200, data: { data, cursor: 3, next_page: page * size < items.length ? page + 1 : null } } + } + assert.ok(request.pathname.endsWith('/changes')) + const after = Number(request.searchParams.get('after')) + const size = Number(request.searchParams.get('limit')) + const remaining = feed.filter((change) => change.sequence > after) + const data = remaining.slice(0, size) + assert.ok(data.length <= 1, 'change responses must fit one attachment through the native bridge') + return { status: 200, data: { data, cursor: feed.at(-1)?.sequence ?? 3, has_more: remaining.length > size } } + } + } + } + }) + const files = new Map() + let state: any = null + const sync = new CloudSyncCoordinator('vault-1', createCloudSyncClient('https://example.test', 'test-only'), { + scan: async () => [...files.values()], + apply: async (change: any) => { + files.set(change.path, { path: change.path, kind: 'binary', content: change.content }) + } + }, { + load: async () => state, + save: async (next: any) => { state = structuredClone(next) } + }, { + itemId: () => assert.fail('download must not invent an item'), + operationId: () => assert.fail('download must not upload unchanged files') + }) + + await sync.sync() + assert.equal(state.cursor, 3) + assert.equal(files.size, 3) + assert.deepEqual(requests.filter((url) => url.pathname.endsWith('/manifest')).map((url) => url.searchParams.get('page')), ['1', '2', '3']) + + feed = items.map((item, index) => ({ + sequence: 4 + index, item_id: item.item_id, path: item.path, previous_path: item.path, + type: 'upsert', revision: 2, + content: { ...item.content, data: `updated-${index}`, sha256: `updated-hash-${index}` } + })) + requests.length = 0 + await sync.sync() + assert.equal(state.cursor, 6) + assert.deepEqual([...files.values()].map((item) => item.content.sha256), feed.map((change) => change.content.sha256)) + assert.deepEqual(requests.slice(0, 3).map((url) => url.searchParams.get('after')), ['3', '4', '5']) +}) + +it('keeps metadata-only manifest pagination intact', async () => { + const requests: URL[] = [] + const { createCloudSyncClient } = await loadMobileModule('./src/bridge/cloud-sync-client.ts', { + '@capacitor/core': { + registerPlugin: () => ({}), + CapacitorHttp: { request: async ({ url }: { url: string }) => { + requests.push(new URL(url)) + return { status: 200, data: { data: [], cursor: 10, next_page: null } } + } } + } + }) + const client = createCloudSyncClient('https://example.test', 'test-only') + await client.manifest('vault-1', { includeContent: false, page: 2, perPage: 250 }) + await client.manifest('vault-1', { includeContent: false, perPage: 1 }) + assert.equal(requests[0].searchParams.get('per_page'), '250') + assert.equal(requests[0].searchParams.get('page'), '2') + assert.equal(requests[1].searchParams.get('per_page'), '1') + assert.ok(requests.every((url) => url.searchParams.get('include_content') === 'false')) +}) diff --git a/src/bridge/cloud-sync-client.ts b/src/bridge/cloud-sync-client.ts index 909c1d9..bdc2600 100644 --- a/src/bridge/cloud-sync-client.ts +++ b/src/bridge/cloud-sync-client.ts @@ -102,6 +102,19 @@ class MobileCloudSyncApiClient extends CloudSyncApiClient { super(http) } + // Capacitor copies JSON through Java and the WebView. A page containing + // several near-limit attachments can exhaust Android's native heap. + override manifest( + vaultId: string, + options: { includeContent?: boolean; page?: number; perPage?: number } = {} + ) { + return super.manifest(vaultId, options.includeContent ? { ...options, perPage: 1 } : options) + } + + override changes(vaultId: string, after: number, _limit = 100) { + return super.changes(vaultId, after, 1) + } + override async mutate( vaultId: string, body: CloudSyncMutationRequest From d945bf0b42d15b9e9728e246b02a332549580559 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 14 Sep 2026 11:20:43 -0500 Subject: [PATCH 3/4] fix: preserve Cloud sign-in across native app restarts --- src/bridge/mobile-cloud-auth.test.ts | 86 ++++++++++++++++++++++++++++ src/bridge/mobile-cloud-auth.ts | 69 +++++++++++++++++----- tooling/load-mobile-module.ts | 1 + 3 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 src/bridge/mobile-cloud-auth.test.ts diff --git a/src/bridge/mobile-cloud-auth.test.ts b/src/bridge/mobile-cloud-auth.test.ts new file mode 100644 index 0000000..c18ff28 --- /dev/null +++ b/src/bridge/mobile-cloud-auth.test.ts @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict' +import { randomUUID } from 'node:crypto' +import { it } from 'node:test' +import { registerPlugin } from '@capacitor/core' +import { loadMobileModule } from '../../tooling/load-mobile-module.ts' + +const account = { + base_url: 'https://zennotes.org', connected_at: '2026-09-14T12:00:00Z', + user: { name: 'Test', email: 'test@example.test' }, + device: { id: 'test-device', name: 'Test phone', platform: 'android' } +} +const credential = JSON.stringify({ base_url: account.base_url, token: 'test-only', account }) + +async function coldLaunch(saved: Map) { + // Exercise the real Capacitor lazy proxy: concurrent first calls can + // instantiate separate implementations, each with its own key prefix. + const secureStorage = registerPlugin(`TestCloudStorage${randomUUID()}`, { + web: async () => { + await Promise.resolve() + return new class { + prefix = 'capacitor-storage_' + async setKeyPrefix(prefix: string) { this.prefix = prefix } + async setSynchronize(_value: boolean) {} + async setDefaultKeychainAccess(_value: unknown) {} + async getItem(key: string) { return saved.get(this.prefix + key) ?? null } + async setItem(key: string, value: string) { saved.set(this.prefix + key, value) } + async removeItem(key: string) { saved.delete(this.prefix + key) } + }() + } + }) + const api = await loadMobileModule('./src/bridge/mobile-cloud-auth.ts', { + '@capacitor/core': { Capacitor: { isNativePlatform: () => true }, CapacitorHttp: {} }, + '@capacitor/app': { App: { addListener: async () => ({}), getLaunchUrl: async () => null } }, + '@aparajita/capacitor-secure-storage': { + SecureStorage: secureStorage, + KeychainAccess: { whenUnlockedThisDeviceOnly: 'device-only' } + }, + './cloud-sync-client': { createCloudSyncClient: () => assert.fail('status must only read storage') } + }) + await api.configureMobileCloudAuth('test-version') + return api +} + +for (const prefix of ['zennotes.cloud.', 'capacitor-storage_']) { + it(`loads a saved account from ${prefix} on every cold launch`, async () => { + const saved = new Map([[prefix + 'credential', credential]]) + for (let launch = 0; launch < 2; launch++) { + const api = await coldLaunch(saved) + const statuses = await Promise.all([api.getMobileCloudAccountStatus(), api.getMobileCloudAccountStatus()]) + assert.deepEqual(statuses, [{ state: 'connected', account }, { state: 'connected', account }]) + assert.deepEqual([...saved.keys()], ['zennotes.cloud.credential']) + } + }) +} + +it('preserves the canonical account and prevents a legacy credential from returning after logout', async () => { + const saved = new Map([ + ['zennotes.cloud.credential', credential], + ['capacitor-storage_credential', JSON.stringify({ base_url: account.base_url, token: 'old-test-token', account })] + ]) + const api = await coldLaunch(saved) + assert.equal((await api.authenticatedCredential()).token, 'test-only') + await api.logoutMobileCloudAccount() + assert.equal(saved.size, 0) + assert.deepEqual(await (await coldLaunch(saved)).getMobileCloudAccountStatus(), { state: 'disconnected', account: null }) +}) + +it('rejects invalid recovered credentials through the shared auth validator', async () => { + const invalid = JSON.stringify({ base_url: 'https://wrong.example.test', token: 'test-only', account }) + const saved = new Map([['capacitor-storage_credential', invalid]]) + const api = await coldLaunch(saved) + assert.deepEqual(await api.getMobileCloudAccountStatus(), { state: 'disconnected', account: null }) + assert.equal(saved.size, 0) +}) + +it('recovers pending sign-in state across cold launches', async () => { + const pending = JSON.stringify({ + base_url: account.base_url, state: 'test-state', code_verifier: 'a'.repeat(43), expires_at: '2099-01-01T00:00:00Z' + }) + const saved = new Map([['capacitor-storage_pending-auth', pending]]) + for (let launch = 0; launch < 2; launch++) { + const api = await coldLaunch(saved) + assert.deepEqual(await api.getMobileCloudAccountStatus(), { state: 'connecting', account: null }) + assert.deepEqual([...saved.entries()], [['zennotes.cloud.pending-auth', pending]]) + } +}) diff --git a/src/bridge/mobile-cloud-auth.ts b/src/bridge/mobile-cloud-auth.ts index 416a3a7..a5c62f4 100644 --- a/src/bridge/mobile-cloud-auth.ts +++ b/src/bridge/mobile-cloud-auth.ts @@ -34,33 +34,76 @@ const accountListeners = new Set<(status: CloudAccountStatus) => void>() let authFlow: CloudAuthFlow | null = null let callbackQueue = Promise.resolve() -const secureStorageReady = Capacitor.isNativePlatform() - ? Promise.all([ - SecureStorage.setKeyPrefix('zennotes.cloud.'), - SecureStorage.setSynchronize(false), - SecureStorage.setDefaultKeychainAccess(KeychainAccess.whenUnlockedThisDeviceOnly) - ]).then(() => undefined) - : Promise.resolve() +// Lazy and retryable so a transient native storage failure does not poison +// every later account read for the session. +let secureStorageSetup: Promise | null = null +function secureStorageReady(): Promise { + if (!Capacitor.isNativePlatform()) return Promise.resolve() + if (!secureStorageSetup) { + secureStorageSetup = configureSecureStorage() + secureStorageSetup.catch(() => { + secureStorageSetup = null + }) + } + return secureStorageSetup +} + +async function configureSecureStorage(): Promise { + // The Capacitor proxy loads its implementation lazily. Parallel first + // calls can initialize separate instances and lose the configured prefix. + await SecureStorage.setKeyPrefix('zennotes.cloud.') + await SecureStorage.setSynchronize(false) + await SecureStorage.setDefaultKeychainAccess(KeychainAccess.whenUnlockedThisDeviceOnly) + await migrateAuthStoragePrefix() +} + +async function migrateAuthStoragePrefix(): Promise { + const keys = [CREDENTIAL_KEY, PENDING_AUTH_KEY] + const canonical = new Map() + for (const key of keys) canonical.set(key, await SecureStorage.getItem(key)) + + // Affected builds could persist auth under the plugin's default prefix. + // All storage callers await setup, so none can see this temporary prefix. + // CloudAuthFlow still validates every recovered record before using it. + try { + await SecureStorage.setKeyPrefix('capacitor-storage_') + const legacy = new Map() + for (const key of keys) { + const value = await SecureStorage.getItem(key) + if (value !== null) legacy.set(key, value) + } + await SecureStorage.setKeyPrefix('zennotes.cloud.') + for (const [key, value] of legacy) { + if (canonical.get(key) === null) await SecureStorage.setItem(key, value) + } + // Remove superseded records as well, so logout cannot resurrect an older + // credential on the next launch. Copying must finish before removal. + await SecureStorage.setKeyPrefix('capacitor-storage_') + for (const key of legacy.keys()) await SecureStorage.removeItem(key) + } finally { + await SecureStorage.setKeyPrefix('zennotes.cloud.') + } +} const storage: CloudAuthStorage = { async loadPending(): Promise { if (!Capacitor.isNativePlatform()) return null - await secureStorageReady + await secureStorageReady() return parseStoredJson(await SecureStorage.getItem(PENDING_AUTH_KEY)) }, async savePending(pending: CloudAuthPending): Promise { assertNativeCloudAuth() - await secureStorageReady + await secureStorageReady() await SecureStorage.setItem(PENDING_AUTH_KEY, JSON.stringify(pending)) }, async deletePending(): Promise { if (!Capacitor.isNativePlatform()) return - await secureStorageReady + await secureStorageReady() await SecureStorage.removeItem(PENDING_AUTH_KEY) }, async loadCredential(): Promise { if (!Capacitor.isNativePlatform()) return null - await secureStorageReady + await secureStorageReady() const credential = migrateLegacyCloudCredential( parseStoredJson(await SecureStorage.getItem(CREDENTIAL_KEY)) ) @@ -71,13 +114,13 @@ const storage: CloudAuthStorage = { }, async saveCredential(credential: CloudAuthCredential): Promise { assertNativeCloudAuth() - await secureStorageReady + await secureStorageReady() const canonicalCredential = migrateLegacyCloudCredential(credential).value await SecureStorage.setItem(CREDENTIAL_KEY, JSON.stringify(canonicalCredential)) }, async deleteCredential(): Promise { if (!Capacitor.isNativePlatform()) return - await secureStorageReady + await secureStorageReady() await SecureStorage.removeItem(CREDENTIAL_KEY) } } diff --git a/tooling/load-mobile-module.ts b/tooling/load-mobile-module.ts index 98ee3e0..0f91a50 100644 --- a/tooling/load-mobile-module.ts +++ b/tooling/load-mobile-module.ts @@ -23,6 +23,7 @@ export async function loadMobileModule( platform: 'node', format: 'cjs', write: false, + define: { 'import.meta.env': '{}' }, plugins: [{ name: 'mobile-test-boundaries', setup(plugin: any) { From 04367dc841377510313cf77122fcf3beeb34576d Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 14 Sep 2026 11:41:21 -0500 Subject: [PATCH 4/4] chore: bump Android to 1.1.20 (versionCode 22) --- android/app/build.gradle | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- src/bridge/mobile-bridge.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 382fcfb..d39b308 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "md.zennotes" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 21 - versionName "1.1.19" + versionCode 22 + versionName "1.1.20" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/package-lock.json b/package-lock.json index 534ba41..c83856e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zennotes-android", - "version": "1.1.19", + "version": "1.1.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zennotes-android", - "version": "1.1.19", + "version": "1.1.20", "dependencies": { "@aparajita/capacitor-secure-storage": "^8.0.0", "@capacitor/android": "^8.5.1", diff --git a/package.json b/package.json index 7f0697a..7164312 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zennotes-android", "private": true, - "version": "1.1.19", + "version": "1.1.20", "type": "module", "description": "ZenNotes for Android — Capacitor shell over the ZenNotes app core", "homepage": "https://zennotes.org", diff --git a/src/bridge/mobile-bridge.ts b/src/bridge/mobile-bridge.ts index 87a805d..678be3e 100644 --- a/src/bridge/mobile-bridge.ts +++ b/src/bridge/mobile-bridge.ts @@ -131,7 +131,7 @@ import { import { folderForRelativePath, posixNormalize, sanitizeNoteTitle } from './vault-core' import { isPhoneViewport } from '../viewport' -let appVersion = '1.1.19' +let appVersion = '1.1.20' export async function loadNativeAppVersion(): Promise { try {