mobile: fix saving qr-code to gallery

This commit is contained in:
Ammar Ahmed
2026-05-21 13:37:36 +05:00
parent 68d1a1bc34
commit 5fe6331c1f
9 changed files with 131 additions and 18 deletions

View File

@@ -19,6 +19,8 @@
<uses-permission <uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE" android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:node="remove" /> tools:node="remove" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

View File

@@ -21,7 +21,7 @@ import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl"; import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard"; import Clipboard from "@react-native-clipboard/clipboard";
import React, { createRef } from "react"; 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 RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer"; import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage"; import * as ScopedStorage from "react-native-scoped-storage";
@@ -45,6 +45,54 @@ import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy"; import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles"; 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 { class RecoveryKeySheet extends React.Component {
constructor(props) { constructor(props) {
@@ -106,24 +154,25 @@ class RecoveryKeySheet extends React.Component {
} }
saveQRCODE = async () => { saveQRCODE = async () => {
if (Platform.OS === "android" && !(await hasAndroidPermission())) {
ToastManager.show({
message: strings.permissionRequiredToSaveQRCode(),
type: "info"
});
return;
}
this.svg.current?.toDataURL(async (data) => { this.svg.current?.toDataURL(async (data) => {
try { try {
let path;
let fileName = "nn_" + this.user.email + "_recovery_key_qrcode"; let fileName = "nn_" + this.user.email + "_recovery_key_qrcode";
fileName = sanitizeFilename(fileName, { replacement: "_" }); fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".png"; fileName = fileName + ".png";
if (Platform.OS === "android") { const path = RNFetchBlob.fs.dirs.CacheDir + fileName;
await ScopedStorage.createDocument( await RNFetchBlob.fs.writeFile(path, data, "base64");
fileName,
"image/png", await CameraRoll.saveToCameraRoll(`file://` + path);
data,
"base64"
);
} else {
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, data, "base64");
}
ToastManager.show({ ToastManager.show({
heading: strings.recoveryKeyQRCodeSaved(), heading: strings.recoveryKeyQRCodeSaved(),
type: "success", type: "success",

View File

@@ -630,6 +630,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle",
); );
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputPaths = ( outputPaths = (
@@ -643,6 +644,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", "${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}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
@@ -781,6 +783,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle",
); );
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputPaths = ( outputPaths = (
@@ -794,6 +797,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", "${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}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
@@ -839,6 +843,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle",
); );
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputPaths = ( outputPaths = (
@@ -852,6 +857,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", "${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}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;

View File

@@ -1914,6 +1914,34 @@ PODS:
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- SocketRocket - SocketRocket
- Yoga - 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 (1.5.7):
- react-native-config/App (= 1.5.7) - react-native-config/App (= 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-background-actions (from `../node_modules/react-native-background-actions`)
- react-native-begin-background-task (from `../node_modules/react-native-begin-background-task`) - 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-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-config (from `../node_modules/react-native-config`)
- react-native-date-picker (from `../node_modules/react-native-date-picker`) - react-native-date-picker (from `../node_modules/react-native-date-picker`)
- "react-native-document-picker (from `../node_modules/@react-native-documents/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" :path: "../node_modules/react-native-begin-background-task"
react-native-blob-util: react-native-blob-util:
:path: "../node_modules/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: react-native-config:
:path: "../node_modules/react-native-config" :path: "../node_modules/react-native-config"
react-native-date-picker: react-native-date-picker:
@@ -3941,6 +3972,7 @@ SPEC CHECKSUMS:
react-native-background-actions: 40e09df6ea8c7d2753f6c1d75f6f1eee4a9bc35f react-native-background-actions: 40e09df6ea8c7d2753f6c1d75f6f1eee4a9bc35f
react-native-begin-background-task: 2191f2a84b0328932a3d44db4361e666fee781bb react-native-begin-background-task: 2191f2a84b0328932a3d44db4361e666fee781bb
react-native-blob-util: 7946b7e13acf0da5e849dc2f73fcfebe1d981699 react-native-blob-util: 7946b7e13acf0da5e849dc2f73fcfebe1d981699
react-native-cameraroll: bb98380ee21115d5fe1ae0f8b80c86e044613746
react-native-config: 963b5efabc864cf69412e54b5de49b6a23e4af03 react-native-config: 963b5efabc864cf69412e54b5de49b6a23e4af03
react-native-date-picker: 4f4f40f6e65798038bb4b1bff47890c2be69c2e6 react-native-date-picker: 4f4f40f6e65798038bb4b1bff47890c2be69c2e6
react-native-document-picker: d624d3d9bd9311da87f6f7b64aa44f69927d8543 react-native-document-picker: d624d3d9bd9311da87f6f7b64aa44f69927d8543

View File

@@ -37,6 +37,7 @@
"@notesnook/logger": "file:../../packages/logger", "@notesnook/logger": "file:../../packages/logger",
"@notesnook/theme": "file:../../packages/theme", "@notesnook/theme": "file:../../packages/theme",
"@notesnook/themes-server": "file:../../servers/themes", "@notesnook/themes-server": "file:../../servers/themes",
"@react-native-camera-roll/camera-roll": "^7.10.2",
"@react-native-clipboard/clipboard": "^1.16.3", "@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/checkbox": "^0.5.20", "@react-native-community/checkbox": "^0.5.20",
"@react-native-community/datetimepicker": "^8.4.5", "@react-native-community/datetimepicker": "^8.4.5",
@@ -4197,6 +4198,18 @@
"node": ">=14" "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": { "node_modules/@react-native-clipboard/clipboard": {
"version": "1.16.3", "version": "1.16.3",
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz", "resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz",

View File

@@ -55,6 +55,7 @@
"@notesnook/logger": "file:../../packages/logger", "@notesnook/logger": "file:../../packages/logger",
"@notesnook/theme": "file:../../packages/theme", "@notesnook/theme": "file:../../packages/theme",
"@notesnook/themes-server": "file:../../servers/themes", "@notesnook/themes-server": "file:../../servers/themes",
"@react-native-camera-roll/camera-roll": "^7.10.2",
"@react-native-clipboard/clipboard": "^1.16.3", "@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/checkbox": "^0.5.20", "@react-native-community/checkbox": "^0.5.20",
"@react-native-community/datetimepicker": "^8.4.5", "@react-native-community/datetimepicker": "^8.4.5",

View File

@@ -1868,7 +1868,7 @@ msgstr "Copy link"
msgid "Copy link text" msgid "Copy link text"
msgstr "Copy link text" msgstr "Copy link text"
#: src/strings.ts:2694 #: src/strings.ts:2703
msgid "Copy logs" msgid "Copy logs"
msgstr "Copy logs" msgstr "Copy logs"
@@ -4678,6 +4678,10 @@ msgstr "Payment method"
msgid "PDF is password protected" msgid "PDF is password protected"
msgstr "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 #: src/strings.ts:1729
msgid "phone number" msgid "phone number"
msgstr "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." 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." 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." 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." msgstr "We couldn't load this theme. The file appears to be incomplete or missing required theme properties."

View File

@@ -1857,7 +1857,7 @@ msgstr ""
msgid "Copy link text" msgid "Copy link text"
msgstr "" msgstr ""
#: src/strings.ts:2694 #: src/strings.ts:2703
msgid "Copy logs" msgid "Copy logs"
msgstr "" msgstr ""
@@ -4652,6 +4652,10 @@ msgstr ""
msgid "PDF is password protected" msgid "PDF is password protected"
msgstr "" msgstr ""
#: src/strings.ts:2705
msgid "Permission required to save QR-Code to Gallery"
msgstr ""
#: src/strings.ts:1729 #: src/strings.ts:1729
msgid "phone number" msgid "phone number"
msgstr "" 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." msgid "We couldn't load this theme. Please make sure the file is a valid JSON theme file."
msgstr "" 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." msgid "We couldn't load this theme. The file appears to be incomplete or missing required theme properties."
msgstr "" msgstr ""

View File

@@ -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.`, t`We couldn't load this theme. Please make sure the file is a valid JSON theme file.`,
themeMissingRequiredFields: () => themeMissingRequiredFields: () =>
t`We couldn't load this theme. The file appears to be incomplete or missing required theme properties.`, 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`
}; };