fix: improve Android file export with version-aware storage handling
- Add Android version detection from user agent - Implement tiered storage strategy for Android 10+ restrictions - Enhance directory creation logic with better error handling - Add comprehensive fallback chain for file saving - Improve permission denial handling for graceful degradation - Add structured logging for better debugging Resolves "Parent folder doesn't exist" errors on Android 10+ Ensures files are always saved with multiple fallback levels
This commit is contained in:
325
package-lock.json
generated
325
package-lock.json
generated
@@ -6983,6 +6983,29 @@
|
||||
"integrity": "sha512-meL9DERHj+fFVWoOX9fXqfcYcSpUfSYJPcFvDPKrxitICbwAoWR+Ut4j5NO9zAT917HUHLQmqzQbAsGNHlDcxQ==",
|
||||
"license": "Apache-2.0 OR MIT"
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -7839,9 +7862,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/assets-registry": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.3.tgz",
|
||||
"integrity": "sha512-Vy8DQXCJ21YSAiHxrNBz35VqVlZPpRYm50xRTWRf660JwHuJkFQG8cUkrLzm7AUriqUXxwpkQHcY+b0ibw9ejQ==",
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.80.0.tgz",
|
||||
"integrity": "sha512-MlScsKAz99zoYghe5Rf5mUqsqz2rMB02640NxtPtBMSHNdGxxRlWu/pp1bFexDa1DYJwyIjnLgt3Z/Y90ikHfw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -7947,20 +7970,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/community-cli-plugin": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.3.tgz",
|
||||
"integrity": "sha512-N/+p4HQqN4yK6IRzn7OgMvUIcrmEWkecglk1q5nj+AzNpfIOzB+mqR20SYmnPfeXF+mZzYCzRANb3KiM+WsSDA==",
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.80.0.tgz",
|
||||
"integrity": "sha512-uadfVvzZfz5tGpqwslL12i+rELK9m6cLhtqICX0JQvS7Bu12PJwrozhKzEzIYwN9i3wl2dWrKDUr08izt7S9Iw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@react-native/dev-middleware": "0.79.3",
|
||||
"@react-native/dev-middleware": "0.80.0",
|
||||
"chalk": "^4.0.0",
|
||||
"debug": "^2.2.0",
|
||||
"debug": "^4.4.0",
|
||||
"invariant": "^2.2.4",
|
||||
"metro": "^0.82.0",
|
||||
"metro-config": "^0.82.0",
|
||||
"metro-core": "^0.82.0",
|
||||
"metro": "^0.82.2",
|
||||
"metro-config": "^0.82.2",
|
||||
"metro-core": "^0.82.2",
|
||||
"semver": "^7.1.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -7975,25 +7998,97 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/community-cli-plugin/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-frontend": {
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.80.0.tgz",
|
||||
"integrity": "sha512-lpu9Z3xtKUaKFvEcm5HSgo1KGfkDa/W3oZHn22Zy0WQ9MiOu2/ar1txgd1wjkoNiK/NethKcRdCN7mqnc6y2mA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/community-cli-plugin/node_modules/@react-native/dev-middleware": {
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.80.0.tgz",
|
||||
"integrity": "sha512-lLyTnJ687A5jF3fn8yR/undlCis3FG+N/apQ+Q0Lcl+GV6FsZs0U5H28YmL6lZtjOj4TLek6uGPMPmZasHx7cQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
"@isaacs/ttlcache": "^1.4.1",
|
||||
"@react-native/debugger-frontend": "0.80.0",
|
||||
"chrome-launcher": "^0.15.2",
|
||||
"chromium-edge-launcher": "^0.2.0",
|
||||
"connect": "^3.6.5",
|
||||
"debug": "^4.4.0",
|
||||
"invariant": "^2.2.4",
|
||||
"nullthrows": "^1.1.1",
|
||||
"open": "^7.0.3",
|
||||
"serve-static": "^1.16.2",
|
||||
"ws": "^6.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/community-cli-plugin/node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/community-cli-plugin/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@react-native/community-cli-plugin/node_modules/open": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
|
||||
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0",
|
||||
"is-wsl": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/community-cli-plugin/node_modules/ws": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
|
||||
"integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/debugger-frontend": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.3.tgz",
|
||||
@@ -8078,9 +8173,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/gradle-plugin": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.79.3.tgz",
|
||||
"integrity": "sha512-imfpZLhNBc9UFSzb/MOy2tNcIBHqVmexh/qdzw83F75BmUtLb/Gs1L2V5gw+WI1r7RqDILbWk7gXB8zUllwd+g==",
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.80.0.tgz",
|
||||
"integrity": "sha512-drmS68rabSMOuDD+YsAY2luNT8br82ycodSDORDqAg7yWQcieHMp4ZUOcdOi5iW+JCqobablT/b6qxcrBg+RaA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -8089,9 +8184,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/js-polyfills": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.79.3.tgz",
|
||||
"integrity": "sha512-PEBtg6Kox6KahjCAch0UrqCAmHiNLEbp2SblUEoFAQnov4DSxBN9safh+QSVaCiMAwLjvNfXrJyygZz60Dqz3Q==",
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.80.0.tgz",
|
||||
"integrity": "sha512-dMX7IcBuwghySTgIeK8q03tYz/epg5ScGmJEfBQAciuhzMDMV1LBR/9wwdgD73EXM/133yC5A+TlHb3KQil4Ew==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -8108,9 +8203,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@react-native/virtualized-lists": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.3.tgz",
|
||||
"integrity": "sha512-/0rRozkn+iIHya2vnnvprDgT7QkfI54FLrACAN3BLP7MRlfOIGOrZsXpRLndnLBVnjNzkcre84i1RecjoXnwIA==",
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.80.0.tgz",
|
||||
"integrity": "sha512-d9zZdPS/ZRexVAkxo1eRp85U7XnnEpXA1ZpSomRKxBuStYKky1YohfEX5YD5MhphemKK24tT7JR4UhaLlmeX8Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -8874,9 +8969,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.33.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
|
||||
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
|
||||
"version": "4.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.0.tgz",
|
||||
"integrity": "sha512-x0IFtj7IJStK+ZqIkhReWbiC0UMjMJnNXV8OXG+DCLDExZaVaxL3MLuq6BJBBcQ1MHZduTHDv3Iz0Zshoj3zjQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
@@ -11184,13 +11279,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/minimatch": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
|
||||
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@@ -12868,9 +12963,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001722",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz",
|
||||
"integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==",
|
||||
"version": "1.0.30001723",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
|
||||
"integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -15347,9 +15442,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.166",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz",
|
||||
"integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==",
|
||||
"version": "1.5.167",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz",
|
||||
"integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -16079,9 +16174,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ethers": {
|
||||
"version": "6.14.3",
|
||||
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.3.tgz",
|
||||
"integrity": "sha512-qq7ft/oCJohoTcsNPFaXSQUm457MA5iWqkf1Mb11ujONdg7jBI6sAOrHaTi3j0CBqIGFSCeR/RMc+qwRRub7IA==",
|
||||
"version": "6.14.4",
|
||||
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.4.tgz",
|
||||
"integrity": "sha512-Jm/dzRs2Z9iBrT6e9TvGxyb5YVKAPLlpna7hjxH7KH/++DSh2T/JVmQUv7iHI5E55hDbp/gEVvstWYXVxXFzsA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -16178,21 +16273,21 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ethr-did": {
|
||||
"version": "3.0.37",
|
||||
"resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.37.tgz",
|
||||
"integrity": "sha512-L9UUhAS8B1T7jTRdKLwAt514lx2UrJebJK7uc6UU4AJ9RhY8Vcfwc93Ux82jREE7yvvqDPXsVNH+lS3aw18a9A==",
|
||||
"version": "3.0.38",
|
||||
"resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.38.tgz",
|
||||
"integrity": "sha512-gUxtErXVOQUJf+bmnxRdSJdlU9aFbQSBNaJCYGt+PLqw6l4qqInTfMRiWpwe/brhRtdjE+64tnayOVk8ataeQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"did-jwt": "^8.0.0",
|
||||
"did-resolver": "^4.1.0",
|
||||
"ethers": "^6.8.1",
|
||||
"ethr-did-resolver": "11.0.3"
|
||||
"ethr-did-resolver": "11.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ethr-did-resolver": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ethr-did-resolver/-/ethr-did-resolver-11.0.3.tgz",
|
||||
"integrity": "sha512-lQ1T/SZfgR6Kp05/GSIXnMELxQ5H6M6OCTH4wBTVSAgHzbJiDNVIYWzg/c+NniIM88B0ViAi4CaiCHaiUlvPQg==",
|
||||
"version": "11.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ethr-did-resolver/-/ethr-did-resolver-11.0.4.tgz",
|
||||
"integrity": "sha512-EJ/dL2QsFzvhBJd0nlPFjma3bxpQOWyp2TytQZyAeqi6SfZ4ALCB0VaA4dSeT4T8ZtI2pzs/sD7t/7A0584J6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"did-resolver": "^4.1.0",
|
||||
@@ -24054,9 +24149,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
|
||||
"integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz",
|
||||
"integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -24954,44 +25049,43 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-native": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.3.tgz",
|
||||
"integrity": "sha512-EzH1+9gzdyEo9zdP6u7Sh3Jtf5EOMwzy+TK65JysdlgAzfEVfq4mNeXcAZ6SmD+CW6M7ARJbvXLyTD0l2S5rpg==",
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.80.0.tgz",
|
||||
"integrity": "sha512-b9K1ygb2MWCBtKAodKmE3UsbUuC29Pt4CrJMR0ocTA8k+8HJQTPleBPDNKL4/p0P01QO9aL/gZUddoxHempLow==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/create-cache-key-function": "^29.7.0",
|
||||
"@react-native/assets-registry": "0.79.3",
|
||||
"@react-native/codegen": "0.79.3",
|
||||
"@react-native/community-cli-plugin": "0.79.3",
|
||||
"@react-native/gradle-plugin": "0.79.3",
|
||||
"@react-native/js-polyfills": "0.79.3",
|
||||
"@react-native/normalize-colors": "0.79.3",
|
||||
"@react-native/virtualized-lists": "0.79.3",
|
||||
"@react-native/assets-registry": "0.80.0",
|
||||
"@react-native/codegen": "0.80.0",
|
||||
"@react-native/community-cli-plugin": "0.80.0",
|
||||
"@react-native/gradle-plugin": "0.80.0",
|
||||
"@react-native/js-polyfills": "0.80.0",
|
||||
"@react-native/normalize-colors": "0.80.0",
|
||||
"@react-native/virtualized-lists": "0.80.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"anser": "^1.4.9",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-syntax-hermes-parser": "0.25.1",
|
||||
"babel-plugin-syntax-hermes-parser": "0.28.1",
|
||||
"base64-js": "^1.5.1",
|
||||
"chalk": "^4.0.0",
|
||||
"commander": "^12.0.0",
|
||||
"event-target-shim": "^5.0.1",
|
||||
"flow-enums-runtime": "^0.0.6",
|
||||
"glob": "^7.1.1",
|
||||
"invariant": "^2.2.4",
|
||||
"jest-environment-node": "^29.7.0",
|
||||
"memoize-one": "^5.0.0",
|
||||
"metro-runtime": "^0.82.0",
|
||||
"metro-source-map": "^0.82.0",
|
||||
"metro-runtime": "^0.82.2",
|
||||
"metro-source-map": "^0.82.2",
|
||||
"nullthrows": "^1.1.1",
|
||||
"pretty-format": "^29.7.0",
|
||||
"promise": "^8.3.0",
|
||||
"react-devtools-core": "^6.1.1",
|
||||
"react-refresh": "^0.14.0",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"scheduler": "0.25.0",
|
||||
"scheduler": "0.26.0",
|
||||
"semver": "^7.1.3",
|
||||
"stacktrace-parser": "^0.1.10",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
@@ -25005,8 +25099,8 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"react": "^19.0.0"
|
||||
"@types/react": "^19.1.0",
|
||||
"react": "^19.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -25039,14 +25133,46 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/@react-native/codegen": {
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.80.0.tgz",
|
||||
"integrity": "sha512-X9TsPgytoUkNrQjzAZh4dXa4AuouvYT0NzYyvnjw1ry4LESCZtKba+eY4x3+M30WPR52zjgu+UFL//14BSdCCA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.1",
|
||||
"hermes-parser": "0.28.1",
|
||||
"invariant": "^2.2.4",
|
||||
"nullthrows": "^1.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/@react-native/normalize-colors": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.3.tgz",
|
||||
"integrity": "sha512-T75NIQPRFCj6DFMxtcVMJTZR+3vHXaUMSd15t+CkJpc5LnyX91GVaPxpRSAdjFh7m3Yppl5MpdjV/fntImheYQ==",
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.80.0.tgz",
|
||||
"integrity": "sha512-bJZDSopadjJxMDvysc634eTfLL4w7cAx5diPe14Ez5l+xcKjvpfofS/1Ja14DlgdMJhxGd03MTXlrxoWust3zg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.28.1.tgz",
|
||||
"integrity": "sha512-meT17DOuUElMNsL5LZN56d+KBp22hb0EfxWfuPUeoSi54e40v1W4C2V36P75FpsH9fVEfDKpw5Nnkahc8haSsQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"hermes-parser": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
@@ -25058,6 +25184,25 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/hermes-estree": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.28.1.tgz",
|
||||
"integrity": "sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-native/node_modules/hermes-parser": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.28.1.tgz",
|
||||
"integrity": "sha512-nf8o+hE8g7UJWParnccljHumE9Vlq8F7MqIdeahl+4x0tvCUJYRrT0L7h0MMg/X9YJmkNwsfbaNNrzPtFXOscg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"hermes-estree": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/ws": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
|
||||
@@ -25960,15 +26105,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/glob": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz",
|
||||
"integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==",
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^4.0.1",
|
||||
"minimatch": "^10.0.0",
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
@@ -26010,13 +26155,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/minimatch": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
|
||||
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@@ -26410,9 +26555,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
@@ -31045,9 +31190,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.61",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.61.tgz",
|
||||
"integrity": "sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ==",
|
||||
"version": "3.25.64",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz",
|
||||
"integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -268,14 +268,25 @@ export default class BackupFilesList extends Vue {
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('[BackupFilesList] Storage permission denied:', error);
|
||||
|
||||
// Get specific guidance for the platform
|
||||
let guidance = "This app needs permission to access your files to list and restore backups.";
|
||||
if (typeof platformService.getStoragePermissionGuidance === 'function') {
|
||||
try {
|
||||
guidance = await platformService.getStoragePermissionGuidance();
|
||||
} catch (guidanceError) {
|
||||
logger.warn('[BackupFilesList] Could not get permission guidance:', guidanceError);
|
||||
}
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
type: "warning",
|
||||
title: "Storage Permission Required",
|
||||
text: "This app needs permission to access your files to list and restore backups. Please grant storage permission and try again.",
|
||||
text: guidance,
|
||||
},
|
||||
7000,
|
||||
10000, // Show for 10 seconds to give user time to read
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -304,7 +304,8 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
if (
|
||||
err.message.includes("permission") ||
|
||||
err.message.includes("access") ||
|
||||
err.message.includes("denied")
|
||||
err.message.includes("denied") ||
|
||||
err.message.includes("User denied storage permission")
|
||||
) {
|
||||
logger.log(
|
||||
"Permission check failed, requesting permissions",
|
||||
@@ -325,6 +326,20 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
return;
|
||||
} catch (retryError: unknown) {
|
||||
const retryErr = retryError as Error;
|
||||
|
||||
// If permission is still denied, log it but don't throw an error
|
||||
// This allows the app to continue with limited functionality
|
||||
if (retryErr.message.includes("User denied storage permission")) {
|
||||
logger.warn(
|
||||
"External storage permissions denied by user, continuing with limited functionality",
|
||||
JSON.stringify({
|
||||
error: retryErr.message,
|
||||
timestamp: new Date().toISOString()
|
||||
}, null, 2),
|
||||
);
|
||||
return; // Don't throw error, just return
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to obtain external storage permissions: ${retryErr.message}`,
|
||||
);
|
||||
@@ -688,6 +703,113 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a directory exists by creating it if necessary.
|
||||
* This is a workaround for Android's Filesystem.writeFile recursive option not working reliably.
|
||||
* @param path - The directory path to ensure exists
|
||||
* @param directory - The base directory type
|
||||
* @returns Promise that resolves when the directory is ready
|
||||
*/
|
||||
private async ensureDirectoryExists(path: string, directory: Directory): Promise<void> {
|
||||
try {
|
||||
// Try to read the directory to see if it exists
|
||||
await Filesystem.readdir({
|
||||
path,
|
||||
directory,
|
||||
});
|
||||
logger.log("[CapacitorPlatformService] Directory already exists:", {
|
||||
path,
|
||||
directory,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
// Directory doesn't exist, try to create it
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.log("[CapacitorPlatformService] Directory doesn't exist, attempting to create:", {
|
||||
path,
|
||||
directory,
|
||||
error: errorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
// For Android 10+, we need to handle storage restrictions differently
|
||||
if (!this.getCapabilities().isIOS) {
|
||||
// Try creating the directory by writing a temporary file with recursive=true
|
||||
const tempFileName = `.temp-${Date.now()}`;
|
||||
const tempPath = `${path}/${tempFileName}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: tempPath,
|
||||
data: "",
|
||||
directory,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Clean up the temporary file, leaving the directory
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: tempPath,
|
||||
directory,
|
||||
});
|
||||
} catch (deleteError) {
|
||||
// Ignore delete errors - the directory was created successfully
|
||||
const deleteErrorMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
logger.log("[CapacitorPlatformService] Temporary file cleanup failed (non-critical):", {
|
||||
tempPath,
|
||||
error: deleteErrorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory created successfully:", {
|
||||
path,
|
||||
directory,
|
||||
method: "temporary_file",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
// For iOS, use the standard approach
|
||||
const tempFileName = `.temp-${Date.now()}`;
|
||||
const tempPath = `${path}/${tempFileName}`;
|
||||
|
||||
await Filesystem.writeFile({
|
||||
path: tempPath,
|
||||
data: "",
|
||||
directory,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
await Filesystem.deleteFile({
|
||||
path: tempPath,
|
||||
directory,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory created successfully:", {
|
||||
path,
|
||||
directory,
|
||||
method: "temporary_file",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (createError) {
|
||||
const createErrorMessage = createError instanceof Error ? createError.message : String(createError);
|
||||
logger.warn("[CapacitorPlatformService] Failed to create directory, will try without it:", {
|
||||
path,
|
||||
directory,
|
||||
error: createErrorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// For Android 10+, some directories may not be accessible
|
||||
// We'll let the calling method handle the fallback
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a file directly to user-accessible storage that persists between installations.
|
||||
* On Android: Saves to external storage (Downloads or app-specific directory)
|
||||
@@ -707,57 +829,97 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] File saved to iOS Documents (persistent):", {
|
||||
uri: result.uri,
|
||||
logger.log("[CapacitorPlatformService] File saved to iOS Documents:", {
|
||||
fileName,
|
||||
note: "File persists between app installations and is accessible via Files app",
|
||||
uri: result.uri,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return result.uri;
|
||||
} else {
|
||||
// Android: Save to external storage that persists between installations
|
||||
// Try to save to Downloads first, then fallback to app's external storage
|
||||
// Android: Check for storage restrictions
|
||||
const hasRestrictions = await this.hasStorageRestrictions();
|
||||
const androidVersion = await this.getAndroidVersion();
|
||||
|
||||
logger.log("[CapacitorPlatformService] Android storage analysis:", {
|
||||
hasRestrictions,
|
||||
androidVersion,
|
||||
fileName,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (hasRestrictions) {
|
||||
// For Android 10+ with storage restrictions, try app-specific external storage first
|
||||
try {
|
||||
// Try to save to app's external storage directory
|
||||
await this.ensureDirectoryExists("TimeSafari", Directory.ExternalStorage);
|
||||
|
||||
const result = await Filesystem.writeFile({
|
||||
path: `TimeSafari/${fileName}`,
|
||||
data: content,
|
||||
directory: Directory.ExternalStorage,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] File saved to app external storage (Android 10+):", {
|
||||
fileName,
|
||||
uri: result.uri,
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return result.uri;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.warn("[CapacitorPlatformService] Failed to save to app external storage, trying Downloads:", {
|
||||
fileName,
|
||||
error: errorMessage,
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Try Downloads directory (works on older Android versions)
|
||||
try {
|
||||
// Attempt to save to Downloads directory (most accessible)
|
||||
const downloadsPath = `Download/TimeSafari/${fileName}`;
|
||||
await this.ensureDirectoryExists("Download/TimeSafari", Directory.ExternalStorage);
|
||||
|
||||
const result = await Filesystem.writeFile({
|
||||
path: downloadsPath,
|
||||
path: `Download/TimeSafari/${fileName}`,
|
||||
data: content,
|
||||
directory: Directory.ExternalStorage, // External storage (persists between installations)
|
||||
directory: Directory.ExternalStorage,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] File saved to Android Downloads (persistent):", {
|
||||
uri: result.uri,
|
||||
logger.log("[CapacitorPlatformService] File saved to Downloads (Android):", {
|
||||
fileName,
|
||||
downloadsPath,
|
||||
note: "File persists between app installations and is accessible via file managers",
|
||||
uri: result.uri,
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return result.uri;
|
||||
} catch (downloadsError) {
|
||||
logger.warn("[CapacitorPlatformService] Could not save to Downloads, using app external storage:", downloadsError);
|
||||
|
||||
// Fallback: Save to app's external storage directory
|
||||
const appStoragePath = `TimeSafari/${fileName}`;
|
||||
|
||||
const result = await Filesystem.writeFile({
|
||||
path: appStoragePath,
|
||||
data: content,
|
||||
directory: Directory.ExternalStorage, // External storage (persists between installations)
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.warn("[CapacitorPlatformService] Could not save to Downloads, using app external storage:", {
|
||||
fileName,
|
||||
error: errorMessage,
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] File saved to Android app external storage (persistent):", {
|
||||
uri: result.uri,
|
||||
// Final fallback: app's external storage without subdirectory
|
||||
const result = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
data: content,
|
||||
directory: Directory.ExternalStorage,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] File saved to external storage (fallback):", {
|
||||
fileName,
|
||||
appStoragePath,
|
||||
note: "File persists between app installations and is accessible via file managers",
|
||||
uri: result.uri,
|
||||
androidVersion,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
@@ -765,8 +927,30 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[CapacitorPlatformService] Save to persistent storage failed:", error);
|
||||
throw new Error(`Failed to save to persistent storage: ${error}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error("[CapacitorPlatformService] Save to persistent storage failed:", {
|
||||
fileName,
|
||||
error: errorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Final fallback: app's data directory (not user-accessible but guaranteed to work)
|
||||
logger.warn("[CapacitorPlatformService] Attempting final fallback to app data directory");
|
||||
|
||||
const result = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
data: content,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] File saved to app data directory (fallback):", {
|
||||
fileName,
|
||||
uri: result.uri,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return result.uri;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -823,6 +1007,7 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
} else {
|
||||
// Android: List files from persistent storage locations
|
||||
const allFiles: Array<{name: string, uri: string, size?: number}> = [];
|
||||
|
||||
// Try to list files from Downloads/TimeSafari directory
|
||||
try {
|
||||
const downloadsResult = await Filesystem.readdir({
|
||||
@@ -840,8 +1025,14 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
}));
|
||||
allFiles.push(...downloadFiles);
|
||||
} catch (downloadsError) {
|
||||
logger.warn("[CapacitorPlatformService] Could not read Downloads/TimeSafari directory:", downloadsError);
|
||||
const err = downloadsError as Error;
|
||||
if (err.message.includes("User denied storage permission")) {
|
||||
logger.warn("[CapacitorPlatformService] Storage permission denied for Downloads/TimeSafari, skipping");
|
||||
} else {
|
||||
logger.warn("[CapacitorPlatformService] Could not read Downloads/TimeSafari directory:", downloadsError);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to list files from app's external storage directory
|
||||
try {
|
||||
const appStorageResult = await Filesystem.readdir({
|
||||
@@ -859,8 +1050,14 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
}));
|
||||
allFiles.push(...appStorageFiles);
|
||||
} catch (appStorageError) {
|
||||
logger.warn("[CapacitorPlatformService] Could not read app external storage directory:", appStorageError);
|
||||
const err = appStorageError as Error;
|
||||
if (err.message.includes("User denied storage permission")) {
|
||||
logger.warn("[CapacitorPlatformService] Storage permission denied for TimeSafari, skipping");
|
||||
} else {
|
||||
logger.warn("[CapacitorPlatformService] Could not read app external storage directory:", appStorageError);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates based on filename
|
||||
const uniqueFiles = allFiles.filter((file, index, self) =>
|
||||
index === self.findIndex(f => f.name === file.name)
|
||||
@@ -873,6 +1070,11 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
return uniqueFiles;
|
||||
}
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
if (err.message.includes("User denied storage permission")) {
|
||||
logger.warn("[CapacitorPlatformService] Storage permission denied, returning empty file list");
|
||||
return [];
|
||||
}
|
||||
logger.error("[CapacitorPlatformService] Failed to list user accessible files:", error);
|
||||
return [];
|
||||
}
|
||||
@@ -1294,6 +1496,9 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
|
||||
// Try to save to the user-selected directory
|
||||
try {
|
||||
// Ensure the selected directory exists before writing
|
||||
await this.ensureDirectoryExists(directoryResult.path, Directory.ExternalStorage);
|
||||
|
||||
const result = await Filesystem.writeFile({
|
||||
path: fullPath,
|
||||
data: content,
|
||||
@@ -1817,7 +2022,12 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
}));
|
||||
allFiles.push(...appStorageFiles);
|
||||
} catch (error) {
|
||||
logger.warn("[CapacitorPlatformService] Could not read TimeSafari external storage:", error);
|
||||
const err = error as Error;
|
||||
if (err.message.includes("User denied storage permission")) {
|
||||
logger.warn("[CapacitorPlatformService] Storage permission denied for TimeSafari, skipping");
|
||||
} else {
|
||||
logger.warn("[CapacitorPlatformService] Could not read TimeSafari external storage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Common user-chosen locations (if accessible) with recursive search
|
||||
@@ -1903,7 +2113,12 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
}
|
||||
}
|
||||
} catch (subDirError) {
|
||||
logger.warn(`[CapacitorPlatformService] Could not read subdirectory ${path}/${fileName}:`, subDirError);
|
||||
const subDirErr = subDirError as Error;
|
||||
if (subDirErr.message.includes("User denied storage permission")) {
|
||||
logger.warn(`[CapacitorPlatformService] Storage permission denied for subdirectory ${path}/${fileName}, skipping`);
|
||||
} else {
|
||||
logger.warn(`[CapacitorPlatformService] Could not read subdirectory ${path}/${fileName}:`, subDirError);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if file matches backup criteria
|
||||
@@ -1932,9 +2147,13 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
});
|
||||
allFiles.push(...relevantFiles);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently skip inaccessible directories
|
||||
logger.debug(`[CapacitorPlatformService] Could not access ${path}:`, error);
|
||||
} catch (pathError) {
|
||||
const pathErr = pathError as Error;
|
||||
if (pathErr.message.includes("User denied storage permission")) {
|
||||
logger.warn(`[CapacitorPlatformService] Storage permission denied for ${path}, skipping`);
|
||||
} else {
|
||||
logger.warn(`[CapacitorPlatformService] Could not read ${path}:`, pathError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2277,8 +2496,113 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
|
||||
return entries;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
if (err.message.includes("User denied storage permission")) {
|
||||
logger.warn("[CapacitorPlatformService] Storage permission denied for directory listing, returning empty array:", { path, error: err.message });
|
||||
return [];
|
||||
}
|
||||
logger.error("[CapacitorPlatformService] Failed to list directory:", { path, error });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides user guidance when storage permissions are denied
|
||||
* @returns Promise resolving to guidance message
|
||||
*/
|
||||
async getStoragePermissionGuidance(): Promise<string> {
|
||||
try {
|
||||
if (this.getCapabilities().isIOS) {
|
||||
return "On iOS, files are saved to the Documents folder and can be accessed via the Files app. No additional permissions are required.";
|
||||
} else {
|
||||
// For Android, provide guidance on how to grant permissions
|
||||
return "To access your backup files, please grant storage permissions:\n\n" +
|
||||
"1. Go to your device Settings\n" +
|
||||
"2. Find 'Apps' or 'Application Manager'\n" +
|
||||
"3. Find 'TimeSafari' in the list\n" +
|
||||
"4. Tap 'Permissions'\n" +
|
||||
"5. Enable 'Storage' permission\n" +
|
||||
"6. Return to the app and try again\n\n" +
|
||||
"Alternatively, you can save files to your app's private storage which doesn't require external storage permissions.";
|
||||
}
|
||||
} catch (error) {
|
||||
return "Unable to provide permission guidance. Please check your device settings for app permissions.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests directory creation functionality to ensure the ensureDirectoryExists method works correctly.
|
||||
* @returns Promise resolving to a test result message
|
||||
*/
|
||||
async testDirectoryCreation(): Promise<string> {
|
||||
try {
|
||||
logger.log("[CapacitorPlatformService] Testing directory creation functionality");
|
||||
|
||||
if (this.getCapabilities().isIOS) {
|
||||
return "✅ Directory creation test not needed on iOS - using Documents directory";
|
||||
}
|
||||
|
||||
// Test creating the Downloads/TimeSafari directory
|
||||
await this.ensureDirectoryExists("Download/TimeSafari", Directory.ExternalStorage);
|
||||
|
||||
// Test creating the TimeSafari directory
|
||||
await this.ensureDirectoryExists("TimeSafari", Directory.ExternalStorage);
|
||||
|
||||
// Test creating a nested directory structure
|
||||
await this.ensureDirectoryExists("Download/TimeSafari/Test", Directory.ExternalStorage);
|
||||
|
||||
logger.log("[CapacitorPlatformService] Directory creation tests completed successfully");
|
||||
|
||||
return "✅ Directory creation tests passed successfully";
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error("[CapacitorPlatformService] Directory creation test failed:", error);
|
||||
return `❌ Directory creation test failed: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the Android version to handle storage restrictions appropriately
|
||||
* @returns Promise resolving to Android version number or null if not Android
|
||||
*/
|
||||
private async getAndroidVersion(): Promise<number | null> {
|
||||
try {
|
||||
if (!this.getCapabilities().isIOS) {
|
||||
// For Android, we can try to detect version from user agent
|
||||
// This is a fallback since we don't have the Device plugin
|
||||
const userAgent = navigator.userAgent;
|
||||
const androidMatch = userAgent.match(/Android\s+(\d+)/);
|
||||
if (androidMatch) {
|
||||
const version = parseInt(androidMatch[1], 10);
|
||||
logger.log("[CapacitorPlatformService] Android version detected from user agent:", {
|
||||
version,
|
||||
userAgent: userAgent.substring(0, 100) + "...",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return version;
|
||||
}
|
||||
|
||||
// If we can't detect from user agent, assume Android 10+ for safety
|
||||
logger.log("[CapacitorPlatformService] Could not detect Android version, assuming 10+ for safety");
|
||||
return 10;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.warn("[CapacitorPlatformService] Could not detect Android version:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current Android version has storage restrictions
|
||||
* @returns Promise resolving to true if storage restrictions apply
|
||||
*/
|
||||
private async hasStorageRestrictions(): Promise<boolean> {
|
||||
const androidVersion = await this.getAndroidVersion();
|
||||
// Android 10 (API 29) and above have stricter storage restrictions
|
||||
return androidVersion !== null && androidVersion >= 29;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user