diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml
index 7394208cf..4110a2ead 100644
--- a/apps/mobile/android/app/src/main/AndroidManifest.xml
+++ b/apps/mobile/android/app/src/main/AndroidManifest.xml
@@ -19,6 +19,8 @@
+
+
diff --git a/apps/mobile/app/components/sheets/recovery-key/index.jsx b/apps/mobile/app/components/sheets/recovery-key/index.jsx
index 7b0825d0e..eb12874c2 100644
--- a/apps/mobile/app/components/sheets/recovery-key/index.jsx
+++ b/apps/mobile/app/components/sheets/recovery-key/index.jsx
@@ -21,7 +21,7 @@ import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { createRef } from "react";
-import { Platform, View } from "react-native";
+import { PermissionsAndroid, Platform, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
@@ -45,6 +45,54 @@ import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
+import { CameraRoll } from "@react-native-camera-roll/camera-roll";
+
+async function hasAndroidPermission() {
+ const getCheckPermissionPromise = () => {
+ if (Platform.Version >= 33) {
+ return Promise.all([
+ PermissionsAndroid.check(
+ PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES
+ ),
+ PermissionsAndroid.check(
+ PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO
+ )
+ ]).then(
+ ([hasReadMediaImagesPermission, hasReadMediaVideoPermission]) =>
+ hasReadMediaImagesPermission && hasReadMediaVideoPermission
+ );
+ } else {
+ return PermissionsAndroid.check(
+ PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE
+ );
+ }
+ };
+
+ const hasPermission = await getCheckPermissionPromise();
+ if (hasPermission) {
+ return true;
+ }
+ const getRequestPermissionPromise = () => {
+ if (Platform.Version >= 33) {
+ return PermissionsAndroid.requestMultiple([
+ PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES,
+ PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO
+ ]).then(
+ (statuses) =>
+ statuses[PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES] ===
+ PermissionsAndroid.RESULTS.GRANTED &&
+ statuses[PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO] ===
+ PermissionsAndroid.RESULTS.GRANTED
+ );
+ } else {
+ return PermissionsAndroid.request(
+ PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE
+ ).then((status) => status === PermissionsAndroid.RESULTS.GRANTED);
+ }
+ };
+
+ return await getRequestPermissionPromise();
+}
class RecoveryKeySheet extends React.Component {
constructor(props) {
@@ -106,24 +154,25 @@ class RecoveryKeySheet extends React.Component {
}
saveQRCODE = async () => {
+ if (Platform.OS === "android" && !(await hasAndroidPermission())) {
+ ToastManager.show({
+ message: strings.permissionRequiredToSaveQRCode(),
+ type: "info"
+ });
+ return;
+ }
+
this.svg.current?.toDataURL(async (data) => {
try {
- let path;
let fileName = "nn_" + this.user.email + "_recovery_key_qrcode";
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".png";
- if (Platform.OS === "android") {
- await ScopedStorage.createDocument(
- fileName,
- "image/png",
- data,
- "base64"
- );
- } else {
- path = await filesystem.checkAndCreateDir("/");
- await RNFetchBlob.fs.writeFile(path + fileName, data, "base64");
- }
+ const path = RNFetchBlob.fs.dirs.CacheDir + fileName;
+ await RNFetchBlob.fs.writeFile(path, data, "base64");
+
+ await CameraRoll.saveToCameraRoll(`file://` + path);
+
ToastManager.show({
heading: strings.recoveryKeyQRCodeSaved(),
type: "success",
diff --git a/apps/mobile/ios/Notesnook.xcodeproj/project.pbxproj b/apps/mobile/ios/Notesnook.xcodeproj/project.pbxproj
index 078dfb5c6..da403f4f2 100644
--- a/apps/mobile/ios/Notesnook.xcodeproj/project.pbxproj
+++ b/apps/mobile/ios/Notesnook.xcodeproj/project.pbxproj
@@ -630,6 +630,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -643,6 +644,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -781,6 +783,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -794,6 +797,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -839,6 +843,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -852,6 +857,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock
index bd63c44de..f88f96b8f 100644
--- a/apps/mobile/ios/Podfile.lock
+++ b/apps/mobile/ios/Podfile.lock
@@ -1914,6 +1914,34 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
+ - react-native-cameraroll (7.10.2):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
- react-native-config (1.5.7):
- react-native-config/App (= 1.5.7)
- react-native-config/App (1.5.7):
@@ -3522,6 +3550,7 @@ DEPENDENCIES:
- react-native-background-actions (from `../node_modules/react-native-background-actions`)
- react-native-begin-background-task (from `../node_modules/react-native-begin-background-task`)
- react-native-blob-util (from `../node_modules/react-native-blob-util`)
+ - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
- react-native-config (from `../node_modules/react-native-config`)
- react-native-date-picker (from `../node_modules/react-native-date-picker`)
- "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)"
@@ -3717,6 +3746,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-begin-background-task"
react-native-blob-util:
:path: "../node_modules/react-native-blob-util"
+ react-native-cameraroll:
+ :path: "../node_modules/@react-native-camera-roll/camera-roll"
react-native-config:
:path: "../node_modules/react-native-config"
react-native-date-picker:
@@ -3941,6 +3972,7 @@ SPEC CHECKSUMS:
react-native-background-actions: 40e09df6ea8c7d2753f6c1d75f6f1eee4a9bc35f
react-native-begin-background-task: 2191f2a84b0328932a3d44db4361e666fee781bb
react-native-blob-util: 7946b7e13acf0da5e849dc2f73fcfebe1d981699
+ react-native-cameraroll: bb98380ee21115d5fe1ae0f8b80c86e044613746
react-native-config: 963b5efabc864cf69412e54b5de49b6a23e4af03
react-native-date-picker: 4f4f40f6e65798038bb4b1bff47890c2be69c2e6
react-native-document-picker: d624d3d9bd9311da87f6f7b64aa44f69927d8543
diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json
index 0cdce8fde..f2051c38d 100644
--- a/apps/mobile/package-lock.json
+++ b/apps/mobile/package-lock.json
@@ -37,6 +37,7 @@
"@notesnook/logger": "file:../../packages/logger",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/themes-server": "file:../../servers/themes",
+ "@react-native-camera-roll/camera-roll": "^7.10.2",
"@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/checkbox": "^0.5.20",
"@react-native-community/datetimepicker": "^8.4.5",
@@ -4197,6 +4198,18 @@
"node": ">=14"
}
},
+ "node_modules/@react-native-camera-roll/camera-roll": {
+ "version": "7.10.2",
+ "resolved": "https://registry.npmjs.org/@react-native-camera-roll/camera-roll/-/camera-roll-7.10.2.tgz",
+ "integrity": "sha512-XgJQJDFUycmqSX+MH7vTcRigQwEIQNLIu1GvOngCZRwlSV2mF61UzeruSmmHwkBcGnHZFXkKg9fil0FQVfyglw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.17.0"
+ },
+ "peerDependencies": {
+ "react-native": ">=0.59"
+ }
+ },
"node_modules/@react-native-clipboard/clipboard": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz",
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index fd8f232f2..df50a416f 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -55,6 +55,7 @@
"@notesnook/logger": "file:../../packages/logger",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/themes-server": "file:../../servers/themes",
+ "@react-native-camera-roll/camera-roll": "^7.10.2",
"@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/checkbox": "^0.5.20",
"@react-native-community/datetimepicker": "^8.4.5",
diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po
index 33aac399f..752dbf124 100644
--- a/packages/intl/locale/en.po
+++ b/packages/intl/locale/en.po
@@ -1868,7 +1868,7 @@ msgstr "Copy link"
msgid "Copy link text"
msgstr "Copy link text"
-#: src/strings.ts:2694
+#: src/strings.ts:2703
msgid "Copy logs"
msgstr "Copy logs"
@@ -4678,6 +4678,10 @@ msgstr "Payment method"
msgid "PDF is password protected"
msgstr "PDF is password protected"
+#: src/strings.ts:2705
+msgid "Permission required to save QR-Code to Gallery"
+msgstr "Permission required to save QR-Code to Gallery"
+
#: src/strings.ts:1729
msgid "phone number"
msgstr "phone number"
@@ -7281,7 +7285,7 @@ msgstr "We are sorry, it seems that the app crashed due to an error. You can sub
msgid "We couldn't load this theme. Please make sure the file is a valid JSON theme file."
msgstr "We couldn't load this theme. Please make sure the file is a valid JSON theme file."
-#: src/strings.ts:2693
+#: src/strings.ts:2702
msgid "We couldn't load this theme. The file appears to be incomplete or missing required theme properties."
msgstr "We couldn't load this theme. The file appears to be incomplete or missing required theme properties."
diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po
index 20a8dae61..becdf37c1 100644
--- a/packages/intl/locale/pseudo-LOCALE.po
+++ b/packages/intl/locale/pseudo-LOCALE.po
@@ -1857,7 +1857,7 @@ msgstr ""
msgid "Copy link text"
msgstr ""
-#: src/strings.ts:2694
+#: src/strings.ts:2703
msgid "Copy logs"
msgstr ""
@@ -4652,6 +4652,10 @@ msgstr ""
msgid "PDF is password protected"
msgstr ""
+#: src/strings.ts:2705
+msgid "Permission required to save QR-Code to Gallery"
+msgstr ""
+
#: src/strings.ts:1729
msgid "phone number"
msgstr ""
@@ -7231,7 +7235,7 @@ msgstr "<<<<<<< HEAD"
msgid "We couldn't load this theme. Please make sure the file is a valid JSON theme file."
msgstr ""
-#: src/strings.ts:2693
+#: src/strings.ts:2702
msgid "We couldn't load this theme. The file appears to be incomplete or missing required theme properties."
msgstr ""
diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts
index aeb5e97f1..d5daa0159 100644
--- a/packages/intl/src/strings.ts
+++ b/packages/intl/src/strings.ts
@@ -2700,5 +2700,7 @@ Continue without attachments?`,
t`We couldn't load this theme. Please make sure the file is a valid JSON theme file.`,
themeMissingRequiredFields: () =>
t`We couldn't load this theme. The file appears to be incomplete or missing required theme properties.`,
- copyLogs: () => t`Copy logs`
+ copyLogs: () => t`Copy logs`,
+ permissionRequiredToSaveQRCode: () =>
+ t`Permission required to save QR-Code to Gallery`
};