Add merchant logo handle print (#459)
* adding dependencies * updated sharing * adding logo to store settings page * adding pdf export to instructions page * adding trasnlations * fixing eslint
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
} from '../../../../constants/theme';
|
||||
import { useExpandedNavbar } from './hooks/useExpandedNavbar';
|
||||
import ThemeIcon from '../../../../functions/CustomElements/themeIcon';
|
||||
import { shareMessage } from '../../../../functions/handleShare';
|
||||
|
||||
export default function ExpandedContactsPage(props) {
|
||||
const navigate = useNavigation();
|
||||
@@ -114,7 +115,7 @@ export default function ExpandedContactsPage(props) {
|
||||
onPress={() => {
|
||||
if (selectedContact?.isLNURL || !selectedContact?.uniqueName)
|
||||
return;
|
||||
Share.share({
|
||||
shareMessage({
|
||||
message: `${t('share.contact')}\nhttps://blitzwalletapp.com/u/${
|
||||
selectedContact?.uniqueName
|
||||
}`,
|
||||
|
||||
@@ -91,7 +91,6 @@ export default function ConfirmExportPayments({
|
||||
csvData,
|
||||
fileName,
|
||||
'text/csv',
|
||||
navigate,
|
||||
);
|
||||
|
||||
navigate.goBack();
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import GetThemeColors from '../../../../../../hooks/themeColors';
|
||||
import { useImageCache } from '../../../../../../../context-store/imageCache';
|
||||
import { useMemo, useState } from 'react';
|
||||
import * as ImageManipulator from 'expo-image-manipulator';
|
||||
import { getImageFromLibrary } from '../../../../../../functions/imagePickerWrapper';
|
||||
import {
|
||||
deleteDatabaseImage,
|
||||
setDatabaseIMG,
|
||||
} from '../../../../../../../db/photoStorage';
|
||||
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { COLORS, SIZES } from '../../../../../../constants';
|
||||
import { Image } from 'expo-image';
|
||||
import FullLoadingScreen from '../../../../../../functions/CustomElements/loadingScreen';
|
||||
import { ThemeText } from '../../../../../../functions/CustomElements';
|
||||
import CustomButton from '../../../../../../functions/CustomElements/button';
|
||||
import ThemeIcon from '../../../../../../functions/CustomElements/themeIcon';
|
||||
|
||||
export default function BrandLogoUploader({
|
||||
onLogoChange,
|
||||
onLogoRemove,
|
||||
masterInfoObject,
|
||||
brandLogoUri,
|
||||
brandLogoUpdated,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigation();
|
||||
const { backgroundOffset } = GetThemeColors();
|
||||
const { refreshCache, removeProfileImageFromCache } = useImageCache();
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const getPOSImageKey = () => `${masterInfoObject.uuid}_POS`;
|
||||
|
||||
const resizeImage = async imgURL => {
|
||||
try {
|
||||
const { width: originalWidth, height: originalHeight } = imgURL;
|
||||
const photoWidth = originalWidth;
|
||||
const photoHeight = originalHeight;
|
||||
const targetSize = 400;
|
||||
|
||||
const smallerDimension = Math.min(photoWidth, photoHeight);
|
||||
const cropSize = smallerDimension;
|
||||
const cropX = (photoWidth - cropSize) / 2;
|
||||
const cropY = (photoHeight - cropSize) / 2;
|
||||
|
||||
const manipulator = ImageManipulator.ImageManipulator.manipulate(
|
||||
imgURL.uri,
|
||||
);
|
||||
|
||||
const cropped = manipulator.crop({
|
||||
originX: cropX,
|
||||
originY: cropY,
|
||||
width: cropSize,
|
||||
height: cropSize,
|
||||
});
|
||||
|
||||
const resized = cropped.resize({
|
||||
width: targetSize,
|
||||
height: targetSize,
|
||||
});
|
||||
|
||||
const image = await resized.renderAsync();
|
||||
const savedImage = await image.saveAsync({
|
||||
compress: 0.4,
|
||||
format: ImageManipulator.SaveFormat.WEBP,
|
||||
});
|
||||
|
||||
return savedImage;
|
||||
} catch (err) {
|
||||
console.log('Error resizing image', err);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const pickImage = async () => {
|
||||
try {
|
||||
const imagePickerResponse = await getImageFromLibrary({ quality: 1 });
|
||||
const { didRun, error, imgURL } = imagePickerResponse;
|
||||
|
||||
if (!didRun) return;
|
||||
|
||||
if (error) {
|
||||
navigate.navigate('ErrorScreen', { errorMessage: t(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
setIsUploading(true);
|
||||
|
||||
const savedImage = await resizeImage(imgURL);
|
||||
|
||||
if (!savedImage.uri) {
|
||||
setIsUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const offsetTime = Date.now() - startTime;
|
||||
const remainingTime = Math.max(0, 700 - offsetTime);
|
||||
|
||||
if (remainingTime > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, remainingTime));
|
||||
}
|
||||
|
||||
const posKey = getPOSImageKey();
|
||||
const didUpload = await setDatabaseIMG(posKey, { uri: savedImage.uri });
|
||||
|
||||
if (didUpload) {
|
||||
await refreshCache(posKey, savedImage.uri);
|
||||
onLogoChange(savedImage.uri);
|
||||
} else {
|
||||
navigate.navigate('ErrorScreen', {
|
||||
errorMessage: t('errormessages.savingImageError'),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error uploading brand logo', err);
|
||||
navigate.navigate('ErrorScreen', {
|
||||
errorMessage: t('errormessages.savingImageError'),
|
||||
});
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveLogo = async () => {
|
||||
try {
|
||||
setIsUploading(true);
|
||||
const posKey = getPOSImageKey();
|
||||
await deleteDatabaseImage(posKey);
|
||||
await removeProfileImageFromCache(posKey);
|
||||
onLogoRemove();
|
||||
} catch (err) {
|
||||
console.error('Error removing brand logo', err);
|
||||
navigate.navigate('ErrorScreen', {
|
||||
errorMessage: t('errormessages.removingImageError'),
|
||||
});
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const brandLogoSource = useMemo(() => {
|
||||
if (!brandLogoUri) return null;
|
||||
|
||||
if (brandLogoUpdated) {
|
||||
const version = new Date(brandLogoUpdated).getTime();
|
||||
if (!isNaN(version)) {
|
||||
return `${brandLogoUri}?v=${version}`;
|
||||
}
|
||||
}
|
||||
|
||||
return brandLogoUri;
|
||||
}, [brandLogoUri, brandLogoUpdated]);
|
||||
|
||||
return (
|
||||
<View style={styles.logoSection}>
|
||||
<ThemeText content={t('settings.posPath.settings.brandLogo')} />
|
||||
<View
|
||||
style={[styles.logoContainer, { backgroundColor: backgroundOffset }]}
|
||||
>
|
||||
{isUploading ? (
|
||||
<FullLoadingScreen
|
||||
showText={false}
|
||||
containerStyles={{ minHeight: 100 }}
|
||||
/>
|
||||
) : brandLogoSource ? (
|
||||
<View style={styles.logoPreviewContainer}>
|
||||
<View style={styles.logoPreview}>
|
||||
<Image
|
||||
source={{ uri: brandLogoSource }}
|
||||
style={styles.logoImage}
|
||||
contentFit="contain"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.logoButtons}>
|
||||
<CustomButton
|
||||
buttonStyles={styles.logoButton}
|
||||
actionFunction={pickImage}
|
||||
textContent={t('settings.posPath.settings.changeLogo')}
|
||||
/>
|
||||
<CustomButton
|
||||
buttonStyles={styles.logoButton}
|
||||
actionFunction={handleRemoveLogo}
|
||||
textContent={t('settings.posPath.settings.removeLogo')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={styles.logoUploadPlaceholder}
|
||||
onPress={pickImage}
|
||||
disabled={isUploading}
|
||||
>
|
||||
<ThemeIcon iconName={'Image'} size={48} />
|
||||
<ThemeText
|
||||
styles={styles.logoUploadText}
|
||||
content={t('settings.posPath.settings.uploadLogo')}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
logoSection: {
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
},
|
||||
logoContainer: {
|
||||
marginTop: 10,
|
||||
borderRadius: 8,
|
||||
padding: 16,
|
||||
},
|
||||
logoPreviewContainer: {
|
||||
gap: 12,
|
||||
},
|
||||
logoPreview: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
logoImage: {
|
||||
width: 128,
|
||||
height: 128,
|
||||
borderRadius: 8,
|
||||
},
|
||||
logoButtons: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
logoButton: {
|
||||
flex: 1,
|
||||
},
|
||||
logoUploadPlaceholder: {
|
||||
paddingVertical: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 2,
|
||||
borderStyle: 'dashed',
|
||||
borderColor: COLORS.gray,
|
||||
borderRadius: 8,
|
||||
},
|
||||
logoUploadText: {
|
||||
marginTop: 8,
|
||||
fontSize: SIZES.small,
|
||||
},
|
||||
});
|
||||
+165
-60
@@ -4,7 +4,6 @@ import {
|
||||
ThemeText,
|
||||
} from '../../../../../functions/CustomElements';
|
||||
import { useGlobalContextProvider } from '../../../../../../context-store/context';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { COLORS, SIZES } from '../../../../../constants/theme';
|
||||
import { CENTER } from '../../../../../constants/styles';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
@@ -12,14 +11,84 @@ import { copyToClipboard } from '../../../../../functions';
|
||||
import { useToast } from '../../../../../../context-store/toastManager';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar';
|
||||
import { ICONS } from '../../../../../constants';
|
||||
import { CONTENT_KEYBOARD_OFFSET } from '../../../../../constants';
|
||||
import { useImageCache } from '../../../../../../context-store/imageCache';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Image } from 'expo-image';
|
||||
import ViewShot from 'react-native-view-shot';
|
||||
import { createPdf } from 'react-native-pdf-from-image';
|
||||
import CustomButton from '../../../../../functions/CustomElements/button';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import {
|
||||
isFileSharingAvailable,
|
||||
shareFile,
|
||||
} from '../../../../../functions/handleShare';
|
||||
|
||||
const normalizeFileUri = path => {
|
||||
if (path.startsWith('file://')) return path;
|
||||
return `file://${path}`;
|
||||
};
|
||||
|
||||
export default function POSInstructionsPath() {
|
||||
const { masterInfoObject } = useGlobalContextProvider();
|
||||
const navigate = useNavigation();
|
||||
const { cache } = useImageCache();
|
||||
const { showToast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const posURL = `https://pay.blitzwalletapp.com/${masterInfoObject.posSettings.storeName}`;
|
||||
const viewShotRef = useRef(null);
|
||||
|
||||
const logoKey = masterInfoObject?.posSettings?.brandLogo;
|
||||
const cachedImageData = logoKey ? cache?.[logoKey] : null;
|
||||
|
||||
const brandLogoUri = cachedImageData?.localUri || null;
|
||||
const brandLogoUpdated = cachedImageData?.updated || null;
|
||||
|
||||
const brandLogoSource = useMemo(() => {
|
||||
if (!brandLogoUri) return null;
|
||||
|
||||
if (brandLogoUpdated) {
|
||||
const version = new Date(brandLogoUpdated).getTime();
|
||||
if (!isNaN(version)) {
|
||||
return `${brandLogoUri}?v=${version}`;
|
||||
}
|
||||
}
|
||||
|
||||
return brandLogoUri;
|
||||
}, [brandLogoUri, brandLogoUpdated]);
|
||||
|
||||
const generatePDFFromView = async () => {
|
||||
try {
|
||||
const imageURI = await viewShotRef.current.capture();
|
||||
|
||||
const pdfName = `Blitz_${t(
|
||||
'settings.posPath.posInstructionsPath.title',
|
||||
)}_${Date.now()}.pdf`;
|
||||
|
||||
const response = await createPdf({
|
||||
imagePaths: [imageURI],
|
||||
name: pdfName,
|
||||
});
|
||||
|
||||
const destinationPath = `${FileSystem.documentDirectory}${pdfName}`;
|
||||
|
||||
await FileSystem.copyAsync({
|
||||
from: normalizeFileUri(response.filePath),
|
||||
to: destinationPath,
|
||||
});
|
||||
|
||||
const isAvailable = await isFileSharingAvailable();
|
||||
if (isAvailable) {
|
||||
await shareFile(destinationPath, {
|
||||
mimeType: 'application/pdf',
|
||||
dialogTitle: t('settings.posPath.posInstructionsPath.title'),
|
||||
UTI: 'com.adobe.pdf',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating/sharing PDF:', error);
|
||||
showToast('Failed to generate PDF');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<GlobalThemeView
|
||||
@@ -30,59 +99,70 @@ export default function POSInstructionsPath() {
|
||||
label={t('settings.posPath.posInstructionsPath.title')}
|
||||
customBackColor={COLORS.lightModeText}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={[styles.headingText, { marginTop: 'auto' }]}
|
||||
content={t('settings.posPath.posInstructionsPath.head1')}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={styles.headingText}
|
||||
content={t('settings.posPath.posInstructionsPath.head2')}
|
||||
/>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<ViewShot
|
||||
style={styles.viewShotPadding}
|
||||
ref={viewShotRef}
|
||||
options={{ format: 'jpg', quality: 0.9 }}
|
||||
>
|
||||
{brandLogoSource && (
|
||||
<Image
|
||||
source={{ uri: brandLogoSource }}
|
||||
style={styles.logoImage}
|
||||
contentFit="contain"
|
||||
/>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
copyToClipboard(posURL, showToast);
|
||||
}}
|
||||
style={styles.qrCodeContainer}
|
||||
>
|
||||
<View style={styles.qrCodeBorder}>
|
||||
<QRCode
|
||||
size={250}
|
||||
quietZone={15}
|
||||
value={posURL}
|
||||
color={COLORS.white}
|
||||
backgroundColor={COLORS.lightModeText}
|
||||
<ThemeText
|
||||
styles={[styles.headingText, { marginTop: 'auto' }]}
|
||||
content={t('settings.posPath.posInstructionsPath.head1')}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
copyToClipboard(posURL, showToast);
|
||||
}}
|
||||
>
|
||||
<ThemeText
|
||||
styles={{
|
||||
textAlign: 'center',
|
||||
marginTop: 10,
|
||||
color: COLORS.lightModeText,
|
||||
}}
|
||||
content={posURL}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<ScrollView
|
||||
style={{ marginTop: 'auto', marginBottom: 'auto', maxHeight: 200 }}
|
||||
>
|
||||
<ThemeText
|
||||
styles={styles.lineItem}
|
||||
content={t('settings.posPath.posInstructionsPath.step1')}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={styles.lineItem}
|
||||
content={t('settings.posPath.posInstructionsPath.step2')}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={styles.headingText}
|
||||
content={t('settings.posPath.posInstructionsPath.head2')}
|
||||
/>
|
||||
|
||||
<ThemeText
|
||||
styles={styles.instructionsText}
|
||||
content={t('settings.posPath.posInstructionsPath.step1')}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
copyToClipboard(posURL, showToast);
|
||||
}}
|
||||
style={styles.qrCodeContainer}
|
||||
>
|
||||
<View style={styles.qrCodeBorder}>
|
||||
<QRCode
|
||||
size={275}
|
||||
quietZone={15}
|
||||
value={posURL}
|
||||
color={COLORS.white}
|
||||
backgroundColor={COLORS.lightModeText}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
copyToClipboard(posURL, showToast);
|
||||
}}
|
||||
>
|
||||
<ThemeText styles={styles.posURLText} content={posURL} />
|
||||
</TouchableOpacity>
|
||||
</ViewShot>
|
||||
</ScrollView>
|
||||
<CustomButton
|
||||
buttonStyles={{
|
||||
backgroundColor: COLORS.lightModeText,
|
||||
...CENTER,
|
||||
marginTop: CONTENT_KEYBOARD_OFFSET,
|
||||
}}
|
||||
textStyles={{ color: COLORS.darkModeText }}
|
||||
actionFunction={generatePDFFromView}
|
||||
textContent={t('constants.print')}
|
||||
/>
|
||||
</GlobalThemeView>
|
||||
);
|
||||
}
|
||||
@@ -94,24 +174,49 @@ const styles = StyleSheet.create({
|
||||
includeFontPadding: false,
|
||||
color: COLORS.lightModeText,
|
||||
},
|
||||
viewShotPadding: {
|
||||
paddingVertical: 20,
|
||||
backgroundColor: COLORS.darkModeText,
|
||||
},
|
||||
qrCodeContainer: {
|
||||
width: 275,
|
||||
height: 275,
|
||||
width: 300,
|
||||
height: 300,
|
||||
borderRadius: 20,
|
||||
...CENTER,
|
||||
marginTop: 20,
|
||||
marginVertical: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
qrCodeBorder: {
|
||||
width: 250,
|
||||
height: 250,
|
||||
width: 275,
|
||||
height: 275,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
lineItem: {
|
||||
instructionsText: {
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
marginVertical: 10,
|
||||
maxWidth: 275,
|
||||
...CENTER,
|
||||
marginTop: 15,
|
||||
},
|
||||
lineItem: {
|
||||
marginVertical: 5,
|
||||
paddingLeft: 10,
|
||||
color: COLORS.lightModeText,
|
||||
},
|
||||
logoImage: {
|
||||
width: 70,
|
||||
height: 70,
|
||||
borderRadius: 8,
|
||||
...CENTER,
|
||||
marginBottom: 15,
|
||||
},
|
||||
posURLText: {
|
||||
width: 250,
|
||||
textAlign: 'center',
|
||||
color: COLORS.lightModeText,
|
||||
fontSize: SIZES.smedium,
|
||||
...CENTER,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import {
|
||||
Keyboard,
|
||||
ScrollView,
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
SIZES,
|
||||
} from '../../../../../constants/theme';
|
||||
import ThemeImage from '../../../../../functions/CustomElements/themeImage';
|
||||
import CheckMarkCircle from '../../../../../functions/CustomElements/checkMarkCircle';
|
||||
import {
|
||||
keyboardGoBack,
|
||||
keyboardNavigate,
|
||||
@@ -41,26 +40,141 @@ import { useGlobalInsets } from '../../../../../../context-store/insetsProvider'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { fiatCurrencies } from '../../../../../functions/currencyOptions';
|
||||
import ThemeIcon from '../../../../../functions/CustomElements/themeIcon';
|
||||
import DropdownMenu from '../../../../../functions/CustomElements/dropdownMenu';
|
||||
import { useImageCache } from '../../../../../../context-store/imageCache';
|
||||
import BrandLogoUploader from './internalComponents/brandLogoUploader';
|
||||
|
||||
const StoreNameInput = ({ value, onChange, onFocus, onBlur }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style={styles.inputSection}>
|
||||
<ThemeText content={t('settings.posPath.settings.storeNameInputDesc')} />
|
||||
<CustomSearchInput
|
||||
setInputText={onChange}
|
||||
inputText={value}
|
||||
placeholderText={t(
|
||||
'settings.posPath.settings.storeNameInputPlaceholder',
|
||||
)}
|
||||
containerStyles={styles.inputContainer}
|
||||
onBlurFunction={onBlur}
|
||||
onFocusFunction={onFocus}
|
||||
shouldDelayBlur={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const CurrencySelector = ({ currentCurrency, onCurrencyChange }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const currencyOptions = fiatCurrencies
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.map(currency => ({
|
||||
label: `${currency.id} - ${currency.info.name}`,
|
||||
value: currency.id,
|
||||
}));
|
||||
|
||||
const selectedCurrencyLabel = currencyOptions.find(
|
||||
opt => opt.value === currentCurrency,
|
||||
)?.label;
|
||||
|
||||
return (
|
||||
<View style={styles.inputSection}>
|
||||
<ThemeText content={t('settings.posPath.settings.displayCurrencyDesc')} />
|
||||
<DropdownMenu
|
||||
options={currencyOptions}
|
||||
selectedValue={selectedCurrencyLabel}
|
||||
onSelect={value => {
|
||||
const selectedOption = currencyOptions.find(
|
||||
opt => opt.label === value.label,
|
||||
);
|
||||
console.log(value);
|
||||
if (selectedOption) {
|
||||
onCurrencyChange(selectedOption.value);
|
||||
}
|
||||
}}
|
||||
dropdownItemCustomStyles={{
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
placeholder={currentCurrency}
|
||||
showClearIcon={false}
|
||||
showVerticalArrowsAbsolute={true}
|
||||
globalContainerStyles={styles.dropdownContainer}
|
||||
translateLabelText={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const ItemsSection = ({ itemCount, showErrorIcon, onNavigate, onInfo }) => {
|
||||
const { t } = useTranslation();
|
||||
const { theme, darkModeType } = useGlobalThemeContext();
|
||||
const { backgroundOffset, backgroundColor } = GetThemeColors();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.addItemContainer,
|
||||
{
|
||||
backgroundColor: theme ? backgroundOffset : COLORS.darkModeText,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ThemeText
|
||||
CustomNumberOfLines={1}
|
||||
styles={styles.itemsText}
|
||||
content={t('settings.posPath.settings.numAddeditems', {
|
||||
number: itemCount,
|
||||
isPlurl:
|
||||
itemCount !== 1 ? t('settings.posPath.settings.plurlEnding') : '',
|
||||
})}
|
||||
/>
|
||||
<TouchableOpacity onPress={onInfo} style={styles.infoButton}>
|
||||
{showErrorIcon ? (
|
||||
<ThemeIcon
|
||||
size={20}
|
||||
colorOverride={
|
||||
theme && darkModeType ? COLORS.darkModeText : COLORS.cancelRed
|
||||
}
|
||||
iconName={'CircleAlert'}
|
||||
/>
|
||||
) : (
|
||||
<ThemeIcon size={20} iconName={'Info'} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={onNavigate}
|
||||
style={[styles.chevronButton, { backgroundColor }]}
|
||||
>
|
||||
<ThemeIcon iconName={'ChevronRight'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default function PosSettingsPage() {
|
||||
const { masterInfoObject, toggleMasterInfoObject } =
|
||||
useGlobalContextProvider();
|
||||
const { isConnectedToTheInternet, screenDimensions } = useAppStatus();
|
||||
const { cache } = useImageCache();
|
||||
const { theme, darkModeType } = useGlobalThemeContext();
|
||||
const { backgroundOffset, textColor, backgroundColor } = GetThemeColors();
|
||||
const { backgroundOffset, backgroundColor } = GetThemeColors();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigation();
|
||||
const [textInput, setTextInput] = useState('');
|
||||
const [storeNameInput, setStoreNameInput] = useState(
|
||||
masterInfoObject?.posSettings?.storeName,
|
||||
);
|
||||
const logoKey = masterInfoObject?.posSettings?.brandLogo;
|
||||
const cachedImageData = logoKey ? cache?.[logoKey] : null;
|
||||
|
||||
const brandLogoUri = cachedImageData?.localUri || null;
|
||||
const brandLogoUpdated = cachedImageData?.updated || null;
|
||||
|
||||
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
|
||||
const { bottomPadding } = useGlobalInsets();
|
||||
|
||||
const savedCurrencies = useMemo(() => {
|
||||
return fiatCurrencies.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}, []);
|
||||
|
||||
const currentCurrency = masterInfoObject?.posSettings?.storeCurrency;
|
||||
const posItemsList = masterInfoObject?.posSettings?.items || [];
|
||||
|
||||
@@ -105,68 +219,66 @@ export default function PosSettingsPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const CurrencyElements = useMemo(() => {
|
||||
return savedCurrencies
|
||||
.filter(currency => {
|
||||
if (
|
||||
currency.info.name
|
||||
.toLowerCase()
|
||||
.startsWith(textInput.toLowerCase()) ||
|
||||
currency.id.toLowerCase().startsWith(textInput.toLowerCase())
|
||||
)
|
||||
return currency;
|
||||
else return false;
|
||||
})
|
||||
.map((item, index) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[
|
||||
styles.currencyContainer,
|
||||
const handleLogoChange = useCallback(() => {
|
||||
savePOSSettings({ brandLogo: masterInfoObject.uuid + '_POS' }, 'brandLogo');
|
||||
}, [savePOSSettings, masterInfoObject.uuid]);
|
||||
|
||||
{
|
||||
marginTop: index === 0 ? 10 : 0,
|
||||
},
|
||||
]}
|
||||
onPress={() => {
|
||||
Keyboard.dismiss();
|
||||
setTextInput('');
|
||||
savePOSSettings({ storeCurrency: item.id }, 'currency');
|
||||
}}
|
||||
>
|
||||
<CheckMarkCircle
|
||||
isActive={
|
||||
item.id?.toLowerCase() === currentCurrency?.toLowerCase()
|
||||
}
|
||||
containerSize={25}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={{
|
||||
color: theme
|
||||
? item.id?.toLowerCase() === currentCurrency?.toLowerCase()
|
||||
? darkModeType
|
||||
? COLORS.darkModeText
|
||||
: COLORS.primary
|
||||
: COLORS.darkModeText
|
||||
: item.id?.toLowerCase() === currentCurrency?.toLowerCase()
|
||||
? COLORS.primary
|
||||
: COLORS.lightModeText,
|
||||
marginLeft: 10,
|
||||
includeFontPadding: false,
|
||||
}}
|
||||
content={`${item.id} - ${item.info.name}`}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
const handleLogoRemove = useCallback(() => {
|
||||
savePOSSettings({ brandLogo: null }, 'brandLogo');
|
||||
}, [savePOSSettings]);
|
||||
|
||||
const handleCurrencyChange = useCallback(
|
||||
currencyId => {
|
||||
console.log(currencyId);
|
||||
Keyboard.dismiss();
|
||||
savePOSSettings({ storeCurrency: currencyId }, 'currency');
|
||||
},
|
||||
[savePOSSettings],
|
||||
);
|
||||
|
||||
const handleItemsInfo = useCallback(() => {
|
||||
navigate.navigate('InformationPopup', {
|
||||
textContent: showErrorIcon
|
||||
? t('settings.posPath.settings.differingCurrencyItemsError', {
|
||||
number: showErrorIcon,
|
||||
})
|
||||
: t('settings.posPath.settings.itemsDescription'),
|
||||
buttonText: t('constants.understandText'),
|
||||
});
|
||||
}, [showErrorIcon, navigate, t]);
|
||||
|
||||
const handleSaveOrOpen = useCallback(() => {
|
||||
if (
|
||||
masterInfoObject.posSettings.storeName.toLowerCase() !==
|
||||
storeNameInput.toLowerCase()
|
||||
) {
|
||||
savePOSSettings(
|
||||
{
|
||||
storeName: storeNameInput.trim(),
|
||||
storeNameLower: storeNameInput.trim().toLowerCase(),
|
||||
},
|
||||
'storeName',
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
openWebBrowser({
|
||||
navigate,
|
||||
link: `https://pay.blitzwalletapp.com/${masterInfoObject.posSettings.storeName}`,
|
||||
});
|
||||
}, [textInput, currentCurrency, masterInfoObject, theme, darkModeType]);
|
||||
}
|
||||
}, [
|
||||
masterInfoObject.posSettings.storeName,
|
||||
storeNameInput,
|
||||
navigate,
|
||||
savePOSSettings,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CustomKeyboardAvoidingView
|
||||
useTouchableWithoutFeedback={true}
|
||||
useStandardWidth={true}
|
||||
>
|
||||
<View style={{ ...styles.topbar }}>
|
||||
<View style={styles.topbar}>
|
||||
<TouchableOpacity
|
||||
style={styles.backArrow}
|
||||
onPress={() => {
|
||||
@@ -179,14 +291,14 @@ export default function PosSettingsPage() {
|
||||
CustomNumberOfLines={1}
|
||||
CustomEllipsizeMode={'tail'}
|
||||
content={t('settings.posPath.settings.title')}
|
||||
styles={{
|
||||
...styles.topBarText,
|
||||
width: screenDimensions.width * 0.95 - 130,
|
||||
}}
|
||||
styles={[
|
||||
styles.topBarText,
|
||||
{ width: screenDimensions.width * 0.95 - 130 },
|
||||
]}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
style={{ position: 'absolute', top: 0, right: 35, zIndex: 1 }}
|
||||
style={styles.infoIcon}
|
||||
onPress={() => {
|
||||
navigate.navigate('POSInstructionsPath');
|
||||
}}
|
||||
@@ -194,7 +306,7 @@ export default function PosSettingsPage() {
|
||||
<ThemeIcon iconName={'Info'} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={{ position: 'absolute', top: 0, right: 0, zIndex: 1 }}
|
||||
style={styles.receiptIcon}
|
||||
onPress={() => {
|
||||
keyboardNavigate(() => {
|
||||
if (!isConnectedToTheInternet) {
|
||||
@@ -215,7 +327,7 @@ export default function PosSettingsPage() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={{ flex: 1, width: '95%', ...CENTER }}
|
||||
style={[styles.scrollView, { ...CENTER }]}
|
||||
contentContainerStyle={{
|
||||
paddingBottom: isKeyboardActive
|
||||
? CONTENT_KEYBOARD_OFFSET
|
||||
@@ -223,144 +335,55 @@ export default function PosSettingsPage() {
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
stickyHeaderIndices={[1]}
|
||||
>
|
||||
<View style={{ marginTop: 20, marginBottom: 10 }}>
|
||||
<ThemeText
|
||||
content={t('settings.posPath.settings.storeNameInputDesc')}
|
||||
/>
|
||||
<CustomSearchInput
|
||||
setInputText={setStoreNameInput}
|
||||
inputText={storeNameInput}
|
||||
placeholderText={t(
|
||||
'settings.posPath.settings.storeNameInputPlaceholder',
|
||||
)}
|
||||
containerStyles={{ marginTop: 10 }}
|
||||
onBlurFunction={() => setIsKeyboardActive(false)}
|
||||
onFocusFunction={() => setIsKeyboardActive(true)}
|
||||
shouldDelayBlur={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Sticky Header Section */}
|
||||
<View style={{ backgroundColor: backgroundColor, paddingTop: 10 }}>
|
||||
<ThemeText
|
||||
content={t('settings.posPath.settings.displayCurrencyDesc')}
|
||||
/>
|
||||
<CustomSearchInput
|
||||
inputText={textInput}
|
||||
setInputText={setTextInput}
|
||||
placeholderText={currentCurrency}
|
||||
containerStyles={{
|
||||
marginTop: 10,
|
||||
marginBottom: CONTENT_KEYBOARD_OFFSET,
|
||||
}}
|
||||
onBlurFunction={() => setIsKeyboardActive(false)}
|
||||
onFocusFunction={() => setIsKeyboardActive(true)}
|
||||
shouldDelayBlur={false}
|
||||
/>
|
||||
</View>
|
||||
{CurrencyElements}
|
||||
</ScrollView>
|
||||
<View
|
||||
style={{
|
||||
...styles.addItemContainer,
|
||||
marginBottom: isKeyboardActive ? CONTENT_KEYBOARD_OFFSET : 20,
|
||||
backgroundColor: theme ? backgroundOffset : COLORS.darkModeText,
|
||||
}}
|
||||
>
|
||||
<ThemeText
|
||||
CustomNumberOfLines={1}
|
||||
styles={{ includeFontPadding: false, marginRight: 5, flexShrink: 1 }}
|
||||
content={t('settings.posPath.settings.numAddeditems', {
|
||||
number: posItemsList.length,
|
||||
isPlurl:
|
||||
posItemsList.length !== 1
|
||||
? t('settings.posPath.settings.plurlEnding')
|
||||
: '',
|
||||
})}
|
||||
<BrandLogoUploader
|
||||
onLogoChange={handleLogoChange}
|
||||
onLogoRemove={handleLogoRemove}
|
||||
masterInfoObject={masterInfoObject}
|
||||
cachedImageData={cachedImageData}
|
||||
brandLogoUri={brandLogoUri}
|
||||
brandLogoUpdated={brandLogoUpdated}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
navigate.navigate('InformationPopup', {
|
||||
textContent: showErrorIcon
|
||||
? t('settings.posPath.settings.differingCurrencyItemsError', {
|
||||
number: showErrorIcon,
|
||||
})
|
||||
: t('settings.posPath.settings.itemsDescription'),
|
||||
buttonText: t('constants.understandText'),
|
||||
})
|
||||
}
|
||||
style={{ marginRight: 5 }}
|
||||
>
|
||||
{showErrorIcon ? (
|
||||
<ThemeIcon
|
||||
size={20}
|
||||
colorOverride={
|
||||
theme && darkModeType ? COLORS.darkModeText : COLORS.cancelRed
|
||||
}
|
||||
iconName={'CircleAlert'}
|
||||
/>
|
||||
) : (
|
||||
<ThemeIcon size={20} iconName={'Info'} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => navigate.navigate('AddPOSItemsPage')}
|
||||
style={{
|
||||
backgroundColor,
|
||||
padding: 5,
|
||||
borderRadius: 8,
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<ThemeIcon iconName={'ChevronRight'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<StoreNameInput
|
||||
value={storeNameInput}
|
||||
onChange={setStoreNameInput}
|
||||
onFocus={() => setIsKeyboardActive(true)}
|
||||
onBlur={() => setIsKeyboardActive(false)}
|
||||
/>
|
||||
|
||||
<CurrencySelector
|
||||
currentCurrency={currentCurrency}
|
||||
onCurrencyChange={handleCurrencyChange}
|
||||
/>
|
||||
<ItemsSection
|
||||
itemCount={posItemsList.length}
|
||||
showErrorIcon={showErrorIcon}
|
||||
onNavigate={() => navigate.navigate('AddPOSItemsPage')}
|
||||
onInfo={handleItemsInfo}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
{!isKeyboardActive && (
|
||||
<>
|
||||
<CustomButton
|
||||
buttonStyles={{
|
||||
width: INSET_WINDOW_WIDTH,
|
||||
alignSelf: 'center',
|
||||
<CustomButton
|
||||
buttonStyles={[
|
||||
styles.mainButton,
|
||||
{
|
||||
backgroundColor: theme ? COLORS.darkModeText : COLORS.primary,
|
||||
marginBottom: isKeyboardActive
|
||||
? CONTENT_KEYBOARD_OFFSET
|
||||
: bottomPadding,
|
||||
}}
|
||||
textStyles={{
|
||||
color: theme ? COLORS.lightModeText : COLORS.darkModeText,
|
||||
}}
|
||||
actionFunction={() => {
|
||||
if (
|
||||
masterInfoObject.posSettings.storeNameLower !==
|
||||
storeNameInput.toLowerCase()
|
||||
) {
|
||||
savePOSSettings(
|
||||
{
|
||||
storeName: storeNameInput.trim(),
|
||||
storeNameLower: storeNameInput.trim().toLowerCase(),
|
||||
},
|
||||
'storeName',
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
openWebBrowser({
|
||||
navigate,
|
||||
link: `https://pay.blitzwalletapp.com/${masterInfoObject.posSettings.storeName}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
textContent={
|
||||
masterInfoObject.posSettings.storeName.toLowerCase() !==
|
||||
storeNameInput.toLowerCase()
|
||||
? t('constants.save')
|
||||
: t('settings.posPath.settings.openPos')
|
||||
}
|
||||
/>
|
||||
</>
|
||||
marginBottom: bottomPadding,
|
||||
},
|
||||
]}
|
||||
textStyles={{
|
||||
color: theme ? COLORS.lightModeText : COLORS.darkModeText,
|
||||
}}
|
||||
actionFunction={handleSaveOrOpen}
|
||||
textContent={
|
||||
masterInfoObject.posSettings.storeName.toLowerCase() !==
|
||||
storeNameInput.toLowerCase()
|
||||
? t('constants.save')
|
||||
: t('settings.posPath.settings.openPos')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</CustomKeyboardAvoidingView>
|
||||
);
|
||||
@@ -378,32 +401,51 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 10,
|
||||
},
|
||||
backArrow: { position: 'absolute', top: 0, left: 0, zIndex: 1 },
|
||||
|
||||
topBarText: {
|
||||
fontSize: SIZES.large,
|
||||
fontFamily: FONT.Title_Regular,
|
||||
textAlign: 'center',
|
||||
...CENTER,
|
||||
},
|
||||
infoIcon: { position: 'absolute', top: 0, right: 35, zIndex: 1 },
|
||||
receiptIcon: { position: 'absolute', top: 0, right: 0, zIndex: 1 },
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
width: '95%',
|
||||
},
|
||||
|
||||
currencyContainer: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
inputSection: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
inputContainer: {
|
||||
marginTop: 10,
|
||||
|
||||
paddingVertical: 10,
|
||||
},
|
||||
dropdownContainer: {
|
||||
marginTop: 5,
|
||||
},
|
||||
addItemContainer: {
|
||||
width: '100%',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
marginTop: 10,
|
||||
},
|
||||
itemsText: {
|
||||
includeFontPadding: false,
|
||||
marginRight: 5,
|
||||
flexShrink: 1,
|
||||
},
|
||||
infoButton: {
|
||||
marginRight: 5,
|
||||
},
|
||||
chevronButton: {
|
||||
padding: 5,
|
||||
borderRadius: 8,
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
mainButton: {
|
||||
width: INSET_WINDOW_WIDTH,
|
||||
marginTop: CONTENT_KEYBOARD_OFFSET,
|
||||
...CENTER,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -148,7 +148,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
verticalArrowsContainer: {
|
||||
height: '100%',
|
||||
width: 20,
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -25,6 +25,7 @@ import getDeepLinkUser from '../../components/admin/homeComponents/contacts/inte
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { getCachedProfileImage } from '../cachedImage';
|
||||
import ThemeIcon from './themeIcon';
|
||||
import { shareMessage } from '../handleShare';
|
||||
|
||||
export default function ShowProfileQr() {
|
||||
const { masterInfoObject } = useGlobalContextProvider();
|
||||
@@ -61,9 +62,9 @@ export default function ShowProfileQr() {
|
||||
|
||||
const handleShare = () => {
|
||||
if (activeType === 'lnurl') {
|
||||
Share.share({ message: currentValue });
|
||||
shareMessage({ message: currentValue });
|
||||
} else {
|
||||
Share.share({ message: t('share.contact') + '\n' + currentValue });
|
||||
shareMessage({ message: t('share.contact') + '\n' + currentValue });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Share } from 'react-native';
|
||||
import { BITCOIN_SATS_ICON } from '../../constants';
|
||||
import i18n from 'i18next';
|
||||
import { shareMessage } from '../handleShare';
|
||||
|
||||
export async function handleGiftCardShare({ amount, giftLink }) {
|
||||
const message = i18n.t('screens.inAccount.giftPages.shareMessage', {
|
||||
@@ -9,5 +9,5 @@ export async function handleGiftCardShare({ amount, giftLink }) {
|
||||
link: giftLink,
|
||||
});
|
||||
|
||||
await Share.share({ message });
|
||||
await shareMessage({ message });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { Platform, Share } from 'react-native';
|
||||
import {
|
||||
documentDirectory,
|
||||
EncodingType,
|
||||
StorageAccessFramework,
|
||||
writeAsStringAsync,
|
||||
readAsStringAsync,
|
||||
} from 'expo-file-system/legacy';
|
||||
import writeAndShareFileToFilesystem from './writeFileToFilesystem';
|
||||
|
||||
/**
|
||||
* Normalize file URI to ensure it starts with file://
|
||||
*/
|
||||
const normalizeFileUri = uri =>
|
||||
uri.startsWith('file://') ? uri : `file://${uri}`;
|
||||
|
||||
/**
|
||||
* Determine encoding type based on file type
|
||||
*/
|
||||
const getEncodingForFileType = fileType => {
|
||||
const binaryTypes = [
|
||||
'application/pdf',
|
||||
'application/zip',
|
||||
'application/octet-stream',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
];
|
||||
return binaryTypes.includes(fileType)
|
||||
? EncodingType.Base64
|
||||
: EncodingType.UTF8;
|
||||
};
|
||||
|
||||
/**
|
||||
* Share a file using expo-sharing
|
||||
*
|
||||
* @param {string} url - The local file URL to share (required)
|
||||
* @param {Object} options - Sharing options
|
||||
* @param {string} options.mimeType - The MIME type of the file
|
||||
* @param {string} options.dialogTitle - Title for the share dialog (Android only)
|
||||
* @param {string} options.UTI - Uniform Type Identifier for the file (iOS only)
|
||||
* @returns {Promise<{success: boolean, error?: string}>}
|
||||
*/
|
||||
export async function shareFile(url, options = {}) {
|
||||
try {
|
||||
const isAvailable = await Sharing.isAvailableAsync();
|
||||
|
||||
if (!isAvailable) {
|
||||
throw new Error('Sharing is not available on this device');
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
throw new Error('File URL is required');
|
||||
}
|
||||
|
||||
const sharingOptions = {};
|
||||
|
||||
if (options.mimeType) sharingOptions.mimeType = options.mimeType;
|
||||
if (options.dialogTitle) sharingOptions.dialogTitle = options.dialogTitle;
|
||||
if (options.UTI) sharingOptions.UTI = options.UTI;
|
||||
|
||||
await Sharing.shareAsync(url, sharingOptions);
|
||||
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
console.error('File sharing failed:', error);
|
||||
return { success: false, error: error.message, originalError: error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Share text/message using React Native's Share API
|
||||
*
|
||||
* @param {Object} content - Content to share
|
||||
* @param {string} content.message - The message to share
|
||||
* @param {string} content.url - URL to share
|
||||
* @param {string} content.title - Title for the share dialog
|
||||
* @param {string} content.type - MIME type (Android only)
|
||||
* @param {Object} options - Additional options
|
||||
* @param {string} options.dialogTitle - Title for share dialog (Android only)
|
||||
* @param {string} options.subject - Subject when sharing via email (iOS only)
|
||||
* @param {string[]} options.excludedActivityTypes - Activity types to exclude (iOS only)
|
||||
* @param {string} options.tintColor - Tint color for share sheet (iOS only)
|
||||
* @returns {Promise<{success: boolean, action?: string, activityType?: string, error?: string}>}
|
||||
*/
|
||||
export async function shareMessage(content, options = {}) {
|
||||
try {
|
||||
if (!content.message && !content.url) {
|
||||
throw new Error('Either message or url is required');
|
||||
}
|
||||
|
||||
const shareContent = {};
|
||||
|
||||
if (content.message) shareContent.message = content.message;
|
||||
if (content.url) shareContent.url = content.url;
|
||||
if (content.title) shareContent.title = content.title;
|
||||
if (content.type) shareContent.type = content.type;
|
||||
|
||||
const shareOptions = {};
|
||||
|
||||
if (options.dialogTitle) shareOptions.dialogTitle = options.dialogTitle;
|
||||
if (options.subject) shareOptions.subject = options.subject;
|
||||
if (options.excludedActivityTypes)
|
||||
shareOptions.excludedActivityTypes = options.excludedActivityTypes;
|
||||
if (options.tintColor) shareOptions.tintColor = options.tintColor;
|
||||
|
||||
const result = await Share.share(shareContent, shareOptions);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: result.action,
|
||||
activityType: result.activityType,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Message sharing failed:', error);
|
||||
return { success: false, error: error.message, originalError: error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file to app's document directory
|
||||
*
|
||||
* @param {string} fileData - The file content (Base64 for binary, UTF8 for text)
|
||||
* @param {string} fileName - Name of the file
|
||||
* @param {string} fileType - MIME type of the file
|
||||
* @returns {Promise<{success: boolean, fileUri?: string, error?: string}>}
|
||||
*/
|
||||
export async function writeFileToDocumentDirectory(
|
||||
fileData,
|
||||
fileName,
|
||||
fileType,
|
||||
) {
|
||||
try {
|
||||
const encoding = getEncodingForFileType(fileType);
|
||||
const fileUri = `${documentDirectory}${fileName}`;
|
||||
|
||||
await writeAsStringAsync(fileUri, fileData, { encoding });
|
||||
|
||||
return { success: true, fileUri, error: null };
|
||||
} catch (error) {
|
||||
console.error('Write to document directory failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'errormessages.writtingFileError',
|
||||
originalError: error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file using Android's Storage Access Framework
|
||||
*
|
||||
* @param {string} fileData - The file content
|
||||
* @param {string} fileName - Name of the file
|
||||
* @param {string} fileType - MIME type of the file
|
||||
* @param {string} sourceFileUri - Source file URI to copy from (optional)
|
||||
* @returns {Promise<{success: boolean, destUri?: string, error?: string}>}
|
||||
*/
|
||||
export async function writeFileUsingSAF(
|
||||
fileData,
|
||||
fileName,
|
||||
fileType,
|
||||
sourceFileUri = null,
|
||||
) {
|
||||
try {
|
||||
// Request directory permissions
|
||||
const permissions =
|
||||
await StorageAccessFramework.requestDirectoryPermissionsAsync();
|
||||
|
||||
if (!permissions.granted) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Storage permission denied',
|
||||
permissionDenied: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Create destination file
|
||||
const destUri = await StorageAccessFramework.createFileAsync(
|
||||
permissions.directoryUri,
|
||||
fileName,
|
||||
fileType,
|
||||
);
|
||||
|
||||
// Determine encoding
|
||||
const encoding = getEncodingForFileType(fileType);
|
||||
|
||||
// If source file URI provided, read from it first
|
||||
let dataToWrite = fileData;
|
||||
if (sourceFileUri) {
|
||||
dataToWrite = await readAsStringAsync(normalizeFileUri(sourceFileUri), {
|
||||
encoding,
|
||||
});
|
||||
}
|
||||
|
||||
// Write to SAF destination
|
||||
await writeAsStringAsync(destUri, dataToWrite, { encoding });
|
||||
|
||||
return { success: true, destUri, error: null };
|
||||
} catch (error) {
|
||||
console.error('SAF write failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'errormessages.savingFileError',
|
||||
originalError: error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal share function that automatically determines the best sharing method
|
||||
*
|
||||
* @param {Object} content - Content to share
|
||||
* @param {string} content.message - Text message to share
|
||||
* @param {string} content.url - URL to share (can be file:// for files or https:// for links)
|
||||
* @param {string} content.title - Title for the share dialog
|
||||
* @param {string} content.type - MIME type
|
||||
* @param {string} content.fileData - File data for write-and-share
|
||||
* @param {string} content.fileName - File name for write-and-share
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function share(content, options = {}) {
|
||||
try {
|
||||
// If fileData and fileName are provided, use write-and-share
|
||||
if (content.fileData && content.fileName) {
|
||||
return await writeAndShareFileToFilesystem(
|
||||
content.fileData,
|
||||
content.fileName,
|
||||
content.type || 'text/plain',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
// If it's a local file URL, use expo-sharing
|
||||
const isFileShare = content.url && content.url.startsWith('file://');
|
||||
|
||||
if (isFileShare) {
|
||||
return await shareFile(content.url, {
|
||||
mimeType: content.type,
|
||||
dialogTitle: content.title,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise use message sharing
|
||||
return await shareMessage(content, options);
|
||||
} catch (error) {
|
||||
console.error('Sharing failed:', error);
|
||||
return { success: false, error: error.message, originalError: error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file sharing is available on the current device
|
||||
*/
|
||||
export async function isFileSharingAvailable() {
|
||||
try {
|
||||
return await Sharing.isAvailableAsync();
|
||||
} catch (error) {
|
||||
console.error('Error checking file sharing availability:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Share,
|
||||
} from 'react-native';
|
||||
import { StyleSheet, View, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import {
|
||||
CENTER,
|
||||
SIZES,
|
||||
@@ -50,6 +44,7 @@ import { useFlashnet } from '../../../context-store/flashnetContext';
|
||||
import { dollarsToSats, satsToDollars } from '../../functions/spark/flashnet';
|
||||
import ThemeIcon from '../../functions/CustomElements/themeIcon';
|
||||
import { useGlobalInsets } from '../../../context-store/insetsProvider';
|
||||
import { shareMessage } from '../../functions/handleShare';
|
||||
|
||||
export default function ReceivePaymentHome(props) {
|
||||
const navigate = useNavigation();
|
||||
@@ -198,7 +193,7 @@ export default function ReceivePaymentHome(props) {
|
||||
if (addressState.isGeneratingInvoice) return;
|
||||
try {
|
||||
isSharingRef.current = true;
|
||||
await Share.share({
|
||||
await shareMessage({
|
||||
message: addressState.generatedAddress,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import Animated, {
|
||||
import { useToast } from '../../../context-store/toastManager';
|
||||
import { copyToClipboard } from '../../functions';
|
||||
import ThemeIcon from '../../functions/CustomElements/themeIcon';
|
||||
import { shareMessage } from '../../functions/handleShare';
|
||||
|
||||
const PREFERENCES = [
|
||||
{
|
||||
@@ -414,7 +415,7 @@ export default function SettingsIndex(props) {
|
||||
<Animated.View style={shareIconStyle}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Share.share({
|
||||
shareMessage({
|
||||
message: `${t(
|
||||
'share.contact',
|
||||
)}\nhttps://blitzwalletapp.com/u/${myContact?.uniqueName}`,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1306,6 +1306,8 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- ExpoSecureStore (15.0.7):
|
||||
- ExpoModulesCore
|
||||
- ExpoSharing (14.0.8):
|
||||
- ExpoModulesCore
|
||||
- ExpoSplashScreen (31.0.10):
|
||||
- ExpoModulesCore
|
||||
- ExpoSQLite (16.0.8):
|
||||
@@ -3439,6 +3441,34 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- react-native-pdf-from-image (0.3.6):
|
||||
- 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-quick-base64 (2.2.2):
|
||||
- React-Core
|
||||
- react-native-quick-crypto (0.7.17):
|
||||
@@ -3560,6 +3590,34 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- react-native-view-shot (4.0.3):
|
||||
- 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-webview (13.15.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@@ -4589,6 +4647,7 @@ DEPENDENCIES:
|
||||
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
||||
- ExpoNetwork (from `../node_modules/expo-network/ios`)
|
||||
- ExpoSecureStore (from `../node_modules/expo-secure-store/ios`)
|
||||
- ExpoSharing (from `../node_modules/expo-sharing/ios`)
|
||||
- ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`)
|
||||
- ExpoSQLite (from `../node_modules/expo-sqlite/ios`)
|
||||
- ExpoSystemUI (from `../node_modules/expo-system-ui/ios`)
|
||||
@@ -4638,10 +4697,12 @@ DEPENDENCIES:
|
||||
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
|
||||
- react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`)
|
||||
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
|
||||
- react-native-pdf-from-image (from `../node_modules/react-native-pdf-from-image`)
|
||||
- react-native-quick-base64 (from `../node_modules/react-native-quick-base64`)
|
||||
- react-native-quick-crypto (from `../node_modules/react-native-quick-crypto`)
|
||||
- react-native-restart (from `../node_modules/react-native-restart`)
|
||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
- react-native-view-shot (from `../node_modules/react-native-view-shot`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
||||
@@ -4789,6 +4850,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/expo-network/ios"
|
||||
ExpoSecureStore:
|
||||
:path: "../node_modules/expo-secure-store/ios"
|
||||
ExpoSharing:
|
||||
:path: "../node_modules/expo-sharing/ios"
|
||||
ExpoSplashScreen:
|
||||
:path: "../node_modules/expo-splash-screen/ios"
|
||||
ExpoSQLite:
|
||||
@@ -4886,6 +4949,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-keyboard-controller"
|
||||
react-native-pager-view:
|
||||
:path: "../node_modules/react-native-pager-view"
|
||||
react-native-pdf-from-image:
|
||||
:path: "../node_modules/react-native-pdf-from-image"
|
||||
react-native-quick-base64:
|
||||
:path: "../node_modules/react-native-quick-base64"
|
||||
react-native-quick-crypto:
|
||||
@@ -4894,6 +4959,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-restart"
|
||||
react-native-safe-area-context:
|
||||
:path: "../node_modules/react-native-safe-area-context"
|
||||
react-native-view-shot:
|
||||
:path: "../node_modules/react-native-view-shot"
|
||||
react-native-webview:
|
||||
:path: "../node_modules/react-native-webview"
|
||||
React-NativeModulesApple:
|
||||
@@ -5027,6 +5094,7 @@ SPEC CHECKSUMS:
|
||||
ExpoModulesCore: 110490095f5bf1c110673078832c6792c08ca8cd
|
||||
ExpoNetwork: b87392dcc24d49a595f186ca25cdcf7293d30275
|
||||
ExpoSecureStore: 9c6571fe3fcb045a671c4011c451546eaaab98fd
|
||||
ExpoSharing: 0d983394ed4a80334bab5a0d5384f75710feb7e8
|
||||
ExpoSplashScreen: cbb839de72110dea1851dd3e85080b7923af2540
|
||||
ExpoSQLite: 7fa091ba5562474093fef09be644161a65e11b3f
|
||||
ExpoSystemUI: 6cd74248a2282adf6dec488a75fa532d69dee314
|
||||
@@ -5107,10 +5175,12 @@ SPEC CHECKSUMS:
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
react-native-keyboard-controller: c4ca61f44d66c2f8987a7e67e9b78e80dc965c45
|
||||
react-native-pager-view: 0e228ec2dfdd87807d125c7bbfb83299edede19c
|
||||
react-native-pdf-from-image: af6ff3b4b4dd840d02fb733e0e7b047b9ebab4ae
|
||||
react-native-quick-base64: 6568199bb2ac8e72ecdfdc73a230fbc5c1d3aac4
|
||||
react-native-quick-crypto: 2c53f72c27485924b444dda124639ee363cf1d69
|
||||
react-native-restart: 0bc732f4461709022a742bb29bcccf6bbc5b4863
|
||||
react-native-safe-area-context: ee1e8e2a7abf737a8d4d9d1a5686a7f2e7466236
|
||||
react-native-view-shot: aab9ffbcc2f01035ee8ffd9a00c773e215b35c8b
|
||||
react-native-webview: d73728424a0e24989d71ffdc6fcf15d5f74ff4a2
|
||||
React-NativeModulesApple: 8c7eb6057b00c191a11ad5ced41826ec5a0e4d78
|
||||
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d
|
||||
|
||||
@@ -122,7 +122,8 @@
|
||||
"bitcoin_upper": "BTC",
|
||||
"dollars_upper": "USD",
|
||||
"rate": "Kurs",
|
||||
"swap": "Swap"
|
||||
"swap": "Swap",
|
||||
"print": "Drucken"
|
||||
},
|
||||
"languages": {
|
||||
"english": "English",
|
||||
@@ -1464,7 +1465,11 @@
|
||||
"itemsDescription": "Das Hinzufügen von Elementen zu Ihrem Kassensystem bedeutet, dass Mitarbeiter die Preise nicht manuell eingeben müssen. Stattdessen können sie einfach auf die Produktnamen klicken, und die von Ihnen festgelegten Preise werden automatisch zum Gesamtbetrag hinzugefügt.",
|
||||
"updateItems": "Elemente aktualisieren",
|
||||
"addItem": "Element hinzufügen",
|
||||
"openPos": "POS öffnen"
|
||||
"openPos": "POS öffnen",
|
||||
"brandLogo": "Markenlogo",
|
||||
"changeLogo": "Logo ändern",
|
||||
"removeLogo": "Entfernen",
|
||||
"uploadLogo": "Tippe hier, um das Markenlogo hochzuladen"
|
||||
},
|
||||
"posInstructionsPath": {
|
||||
"title": "Anweisungen",
|
||||
@@ -1907,7 +1912,8 @@
|
||||
"invalidData": "Die eingegebenen Daten sind ungültig",
|
||||
"paymentError": "Bei der Verarbeitung Ihrer Zahlung ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut. Wenn es weiterhin nicht funktioniert, schließen Sie die App richtig und öffnen Sie sie erneut, bevor Sie es noch einmal versuchen.",
|
||||
"paymentFeeError": "Wir konnten die Zahlungsgebühr nicht abrufen. Bitte versuchen Sie es erneut. Wenn das Problem weiterhin besteht, schließen Sie die App und öffnen Sie sie erneut, bevor Sie es noch einmal versuchen.",
|
||||
"priceImpact": "Bei dieser hohen Zahlung kann sich der endgültige Betrag in USD noch ändern."
|
||||
"priceImpact": "Bei dieser hohen Zahlung kann sich der endgültige Betrag in USD noch ändern.",
|
||||
"removingImageError": "Bild konnte nicht entfernt werden. Bitte versuche es erneut."
|
||||
},
|
||||
"loadingScreen": {
|
||||
"message1": "Bitte schließen Sie die App nicht",
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"dollars_upper": "Dollars",
|
||||
"rate": "Rate",
|
||||
"swap": "Swap",
|
||||
"paymentDescriptionPlaceholder": "Description (optional)"
|
||||
"paymentDescriptionPlaceholder": "Description (optional)",
|
||||
"print": "Print"
|
||||
},
|
||||
|
||||
"languages": {
|
||||
@@ -1495,14 +1496,17 @@
|
||||
"itemsDescription": "Adding items to your point-of-sale system means employees won’t have to type in prices manually. Instead, they can just click the product names, and the prices you set will be added to the total automatically.",
|
||||
"updateItems": "Update Items",
|
||||
"addItem": "Add Item",
|
||||
"openPos": "Open POS"
|
||||
"openPos": "Open POS",
|
||||
"brandLogo": "Brand Logo",
|
||||
"changeLogo": "Change Logo",
|
||||
"removeLogo": "Remove",
|
||||
"uploadLogo": "Tap to upload brand logo"
|
||||
},
|
||||
"posInstructionsPath": {
|
||||
"title": "Instructions",
|
||||
"head1": "How to accept",
|
||||
"head2": "Bitcoin payments",
|
||||
"step1": "1. Scan QR code with your camera",
|
||||
"step2": "2. That's it."
|
||||
"step1": "Scan QR code with your camera"
|
||||
},
|
||||
"totalTipsScreen": {
|
||||
"noTipBalanceError": "User does not have any tips balance.",
|
||||
@@ -1955,7 +1959,8 @@
|
||||
"invalidData": "The data you entered isn’t valid",
|
||||
"paymentError": "Something went wrong while processing your payment. Please try again. If it still doesn’t work, close the app and reopen it before trying again.",
|
||||
"paymentFeeError": "We couldn’t retrieve the payment fee. Please try again. If the issue continues, close the app and reopen it before trying again.",
|
||||
"priceImpact": "This payment is large, so the final amount you receive in Dollars may change."
|
||||
"priceImpact": "This payment is large, so the final amount you receive in Dollars may change.",
|
||||
"removingImageError": "Unable to remove image. Please try again."
|
||||
},
|
||||
|
||||
"loadingScreen": {
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"dollars_upper": "Dólares",
|
||||
"rate": "Tasa",
|
||||
"swap": "Swap",
|
||||
"paymentDescriptionPlaceholder": "Descripción (opcional)"
|
||||
"paymentDescriptionPlaceholder": "Descripción (opcional)",
|
||||
"print": "Imprimir"
|
||||
},
|
||||
|
||||
"languages": {
|
||||
@@ -1327,7 +1328,11 @@
|
||||
"itemsDescription": "Agregar artículos a tu sistema de punto de venta significa que los empleados no tendrán que escribir precios manualmente. En su lugar, pueden hacer clic en los nombres de los productos y los precios que configuraste se añadirán automáticamente al total.",
|
||||
"updateItems": "Actualizar artículos",
|
||||
"addItem": "Añadir artículo",
|
||||
"openPos": "Abrir POS"
|
||||
"openPos": "Abrir POS",
|
||||
"brandLogo": "Logo de la marca",
|
||||
"changeLogo": "Cambiar logo",
|
||||
"removeLogo": "Eliminar",
|
||||
"uploadLogo": "Toca para subir el logo de la marca"
|
||||
},
|
||||
"posInstructionsPath": {
|
||||
"title": "Instrucciones",
|
||||
@@ -1780,7 +1785,8 @@
|
||||
"invalidData": "Los datos proporcionados no son válidos",
|
||||
"paymentError": "Se produjo un error al procesar tu pago. Inténtalo de nuevo. Si aún no funciona, cierra la aplicación y vuelve a abrirla antes de intentarlo nuevamente.",
|
||||
"paymentFeeError": "No pudimos obtener la tarifa de pago. Inténtalo de nuevo. Si el problema continúa, cierra la aplicación y vuelve a abrirla antes de intentarlo nuevamente.",
|
||||
"priceImpact": "Este pago es grande, por lo que el monto final que recibas en dólares puede cambiar."
|
||||
"priceImpact": "Este pago es grande, por lo que el monto final que recibas en dólares puede cambiar.",
|
||||
"removingImageError": "No se pudo eliminar la imagen. Inténtalo de nuevo."
|
||||
},
|
||||
"loadingScreen": {
|
||||
"message1": "Por favor no cierres la aplicación",
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"dollars_upper": "Dollars",
|
||||
"rate": "Taux",
|
||||
"swap": "Swap",
|
||||
"paymentDescriptionPlaceholder": "Description (facultatif)"
|
||||
"paymentDescriptionPlaceholder": "Description (facultatif)",
|
||||
"print": "Imprimer"
|
||||
},
|
||||
"languages": {
|
||||
"english": "English",
|
||||
@@ -1469,7 +1470,11 @@
|
||||
"itemsDescription": "L'ajout d'articles à votre système de point de vente signifie que les employés n'auront pas à saisir les prix manuellement. Ils n'auront qu'à cliquer sur les noms des produits et les prix que vous aurez définis seront automatiquement ajoutés au total.",
|
||||
"updateItems": "Points de mise à jour",
|
||||
"addItem": "Ajouter un article",
|
||||
"openPos": "POS ouvert"
|
||||
"openPos": "POS ouvert",
|
||||
"brandLogo": "Logo de la marque",
|
||||
"changeLogo": "Changer le logo",
|
||||
"removeLogo": "Supprimer",
|
||||
"uploadLogo": "Appuyez pour télécharger le logo de la marque"
|
||||
},
|
||||
"posInstructionsPath": {
|
||||
"title": "Instructions",
|
||||
@@ -1915,7 +1920,8 @@
|
||||
"invalidData": "Les données saisies ne sont pas valides",
|
||||
"paymentError": "Un problème s'est produit lors du traitement de votre paiement. Veuillez réessayer. Si cela ne fonctionne toujours pas, fermez l'application et rouvrez-la avant de réessayer.",
|
||||
"paymentFeeError": "Nous n'avons pas pu récupérer les frais de paiement. Veuillez réessayer. Si le problème persiste, fermez l'application et rouvrez-la avant de réessayer.",
|
||||
"priceImpact": "Ce paiement est important, le montant final que vous recevrez en dollars peut donc changer."
|
||||
"priceImpact": "Ce paiement est important, le montant final que vous recevrez en dollars peut donc changer.",
|
||||
"removingImageError": "Impossible de supprimer l'image. Veuillez réessayer."
|
||||
},
|
||||
"loadingScreen": {
|
||||
"message1": "Ne fermez pas l'application",
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"dollars_upper": "Dollari",
|
||||
"rate": "Tasso",
|
||||
"swap": "Swap",
|
||||
"paymentDescriptionPlaceholder": "Descrizione (facoltativa)"
|
||||
"paymentDescriptionPlaceholder": "Descrizione (facoltativa)",
|
||||
"print": "Stampa"
|
||||
},
|
||||
|
||||
"languages": {
|
||||
@@ -1493,7 +1494,11 @@
|
||||
"itemsDescription": "Aggiungere articoli al tuo sistema Terminale significa che i dipendenti non dovranno digitare manualmente i prezzi. Invece, possono semplicemente cliccare sui nomi dei prodotti e i prezzi che imposti verranno aggiunti automaticamente al totale.",
|
||||
"updateItems": "Aggiorna Articoli",
|
||||
"addItem": "Aggiungi Articolo",
|
||||
"openPos": "Apri Terminale POS"
|
||||
"openPos": "Apri Terminale POS",
|
||||
"brandLogo": "Logo del marchio",
|
||||
"changeLogo": "Cambia logo",
|
||||
"removeLogo": "Rimuovi",
|
||||
"uploadLogo": "Tocca per caricare il logo del marchio"
|
||||
},
|
||||
"posInstructionsPath": {
|
||||
"title": "Istruzioni",
|
||||
@@ -1955,7 +1960,8 @@
|
||||
"invalidData": "I dati forniti non sono validi",
|
||||
"paymentError": "Si è verificato un errore durante l'elaborazione del pagamento. Riprova. Se continua a non funzionare, chiudi l'app e riaprila prima di riprovare.",
|
||||
"paymentFeeError": "Non siamo riusciti a recuperare la commissione di pagamento. Riprova. Se il problema persiste, chiudi l'app e riaprila prima di riprovare.",
|
||||
"priceImpact": "Questo pagamento è elevato, quindi l'importo finale che riceverai in dollari potrebbe cambiare."
|
||||
"priceImpact": "Questo pagamento è elevato, quindi l'importo finale che riceverai in dollari potrebbe cambiare.",
|
||||
"removingImageError": "Impossibile rimuovere l'immagine. Riprova."
|
||||
},
|
||||
|
||||
"loadingScreen": {
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"dollars_upper": "Dólares",
|
||||
"rate": "Cotação",
|
||||
"swap": "Troca",
|
||||
"paymentDescriptionPlaceholder": "Descrição (opcional)"
|
||||
"paymentDescriptionPlaceholder": "Descrição (opcional)",
|
||||
"print": "Imprimir"
|
||||
},
|
||||
|
||||
"languages": {
|
||||
@@ -1491,7 +1492,11 @@
|
||||
"itemsDescription": "Adicionar itens ao seu sistema de ponto de venda significa que os funcionários não precisarão digitar os preços manualmente. Em vez disso, eles podem simplesmente clicar nos nomes dos produtos e os preços definidos serão adicionados ao total automaticamente.",
|
||||
"updateItems": "Atualizar itens",
|
||||
"addItem": "Adicionar item",
|
||||
"openPos": "Abrir PDV"
|
||||
"openPos": "Abrir PDV",
|
||||
"brandLogo": "Logo da marca",
|
||||
"changeLogo": "Alterar logo",
|
||||
"removeLogo": "Remover",
|
||||
"uploadLogo": "Toque para enviar o logo da marca"
|
||||
},
|
||||
"posInstructionsPath": {
|
||||
"title": "Instruções",
|
||||
@@ -1953,7 +1958,8 @@
|
||||
"invalidData": "Os dados fornecidos não são válidos",
|
||||
"paymentError": "Ocorreu um erro ao processar seu pagamento. Tente novamente. Se ainda não funcionar, feche o app e abra-o novamente antes de tentar de novo.",
|
||||
"paymentFeeError": "Não foi possível recuperar a taxa de pagamento. Tente novamente. Se o problema continuar, feche o app e abra-o novamente antes de tentar de novo.",
|
||||
"priceImpact": "Este pagamento é grande, portanto o valor final que você receber em dólares pode mudar."
|
||||
"priceImpact": "Este pagamento é grande, portanto o valor final que você receber em dólares pode mudar.",
|
||||
"removingImageError": "Não foi possível remover a imagem. Tente novamente."
|
||||
},
|
||||
|
||||
"loadingScreen": {
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"dollars_upper": "Доллары",
|
||||
"rate": "Курс",
|
||||
"swap": "Обмен",
|
||||
"paymentDescriptionPlaceholder": "Описание (необязательно)"
|
||||
"paymentDescriptionPlaceholder": "Описание (необязательно)",
|
||||
"print": "Печать"
|
||||
},
|
||||
"languages": {
|
||||
"english": "English",
|
||||
@@ -1466,7 +1467,11 @@
|
||||
"itemsDescription": "Добавьте товары, чтобы не вводить цены вручную.",
|
||||
"updateItems": "Обновить товары",
|
||||
"addItem": "Добавить товар",
|
||||
"openPos": "Открыть POS"
|
||||
"openPos": "Открыть POS",
|
||||
"brandLogo": "Логотип бренда",
|
||||
"changeLogo": "Изменить логотип",
|
||||
"removeLogo": "Удалить",
|
||||
"uploadLogo": "Нажмите, чтобы загрузить логотип бренда"
|
||||
},
|
||||
"posInstructionsPath": {
|
||||
"title": "Инструкции",
|
||||
@@ -1921,7 +1926,8 @@
|
||||
"invalidData": "Введенные данные неверны",
|
||||
"paymentError": "Ошибка обработки платежа. Перезапустите приложение и попробуйте снова.",
|
||||
"paymentFeeError": "Не удалось получить комиссию. Перезапустите приложение.",
|
||||
"priceImpact": "Этот платёж является крупным, поэтому окончательная сумма, которую вы получите в долларах, может измениться."
|
||||
"priceImpact": "Этот платёж является крупным, поэтому окончательная сумма, которую вы получите в долларах, может измениться.",
|
||||
"removingImageError": "Не удалось удалить изображение. Пожалуйста, попробуйте снова."
|
||||
},
|
||||
"loadingScreen": {
|
||||
"message1": "Пожалуйста, не закрывайте приложение",
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
"dollars_upper": "Dollar",
|
||||
"rate": "Kurs",
|
||||
"swap": "Swap",
|
||||
"paymentDescriptionPlaceholder": "Beskrivning (valfritt)"
|
||||
"paymentDescriptionPlaceholder": "Beskrivning (valfritt)",
|
||||
"print": "Skriv ut"
|
||||
},
|
||||
"languages": {
|
||||
"english": "English",
|
||||
@@ -1469,7 +1470,11 @@
|
||||
"itemsDescription": "Om du lägger till artiklar i ditt kassasystem behöver medarbetarna inte skriva in priserna manuellt. Istället kan de bara klicka på produktnamnen och de priser du anger läggs automatiskt till totalsumman.",
|
||||
"updateItems": "Uppdatering av punkter",
|
||||
"addItem": "Lägg till artikel",
|
||||
"openPos": "Öppna POS"
|
||||
"openPos": "Öppna POS",
|
||||
"brandLogo": "Varumärkeslogo",
|
||||
"changeLogo": "Byt logo",
|
||||
"removeLogo": "Ta bort",
|
||||
"uploadLogo": "Tryck för att ladda upp varumärkeslogo"
|
||||
},
|
||||
"posInstructionsPath": {
|
||||
"title": "Instruktioner",
|
||||
@@ -1915,7 +1920,8 @@
|
||||
"invalidData": "De uppgifter du angav är inte giltiga",
|
||||
"paymentError": "Något gick fel när vi behandlade din betalning. Vänligen försök igen. Om det fortfarande inte fungerar stänger du appen och öppnar den igen innan du försöker igen.",
|
||||
"paymentFeeError": "Vi kunde inte hämta betalningsavgiften. Vänligen försök igen. Om problemet kvarstår ska du stänga appen och öppna den igen innan du försöker igen.",
|
||||
"priceImpact": "Den här betalningen är stor, så det slutliga beloppet du får i dollar kan förändras."
|
||||
"priceImpact": "Den här betalningen är stor, så det slutliga beloppet du får i dollar kan förändras.",
|
||||
"removingImageError": "Kunde inte ta bort bilden. Försök igen."
|
||||
},
|
||||
"loadingScreen": {
|
||||
"message1": "Stäng inte appen, snälla",
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"expo-network": "~8.0.7",
|
||||
"expo-notifications": "~0.32.12",
|
||||
"expo-secure-store": "~15.0.7",
|
||||
"expo-sharing": "~14.0.8",
|
||||
"expo-splash-screen": "~31.0.10",
|
||||
"expo-sqlite": "~16.0.8",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
@@ -92,6 +93,7 @@
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
"react-native-localize": "^3.6.0",
|
||||
"react-native-pager-view": "6.9.1",
|
||||
"react-native-pdf-from-image": "^0.3.6",
|
||||
"react-native-qrcode-svg": "^6.3.15",
|
||||
"react-native-quick-base64": "^2.2.2",
|
||||
"react-native-quick-crypto": "^0.7.17",
|
||||
@@ -100,6 +102,7 @@
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-vision-camera": "^4.6.4",
|
||||
"react-native-webview": "13.15.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
|
||||
@@ -4850,6 +4850,7 @@ __metadata:
|
||||
expo-network: ~8.0.7
|
||||
expo-notifications: ~0.32.12
|
||||
expo-secure-store: ~15.0.7
|
||||
expo-sharing: ~14.0.8
|
||||
expo-splash-screen: ~31.0.10
|
||||
expo-sqlite: ~16.0.8
|
||||
expo-status-bar: ~3.0.8
|
||||
@@ -4879,6 +4880,7 @@ __metadata:
|
||||
react-native-keyboard-controller: 1.18.5
|
||||
react-native-localize: ^3.6.0
|
||||
react-native-pager-view: 6.9.1
|
||||
react-native-pdf-from-image: ^0.3.6
|
||||
react-native-qrcode-svg: ^6.3.15
|
||||
react-native-quick-base64: ^2.2.2
|
||||
react-native-quick-crypto: ^0.7.17
|
||||
@@ -4887,6 +4889,7 @@ __metadata:
|
||||
react-native-safe-area-context: ~5.6.0
|
||||
react-native-screens: ~4.16.0
|
||||
react-native-svg: 15.12.1
|
||||
react-native-view-shot: ^4.0.3
|
||||
react-native-vision-camera: ^4.6.4
|
||||
react-native-webview: 13.15.0
|
||||
react-native-worklets: 0.5.1
|
||||
@@ -5735,6 +5738,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-arraybuffer@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "base64-arraybuffer@npm:1.0.2"
|
||||
checksum: 15e6400d2d028bf18be4ed97702b11418f8f8779fb8c743251c863b726638d52f69571d4cc1843224da7838abef0949c670bde46936663c45ad078e89fee5c62
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-js@npm:^1.2.3, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "base64-js@npm:1.5.1"
|
||||
@@ -6705,6 +6715,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-line-break@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "css-line-break@npm:2.1.0"
|
||||
dependencies:
|
||||
utrie: ^1.0.2
|
||||
checksum: 37b1fe632b03be7a287cd394cef8b5285666343443125c510df9cfb6a4734a2c71e154ec8f7bbff72d7c339e1e5872989b1c52d86162aed27d6cc114725bb4d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-select@npm:^5.1.0":
|
||||
version: 5.2.2
|
||||
resolution: "css-select@npm:5.2.2"
|
||||
@@ -8111,6 +8130,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"expo-sharing@npm:~14.0.8":
|
||||
version: 14.0.8
|
||||
resolution: "expo-sharing@npm:14.0.8"
|
||||
peerDependencies:
|
||||
expo: "*"
|
||||
checksum: 8ac54d82328141dc67add5b9db3f16253b42e0e0aa2d39589fffd4d9cf15067e10262b0ab21837b184caae2799de81d74f8b2d129886c50e25550ef4fc6919f3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"expo-splash-screen@npm:~31.0.10":
|
||||
version: 31.0.10
|
||||
resolution: "expo-splash-screen@npm:31.0.10"
|
||||
@@ -8997,6 +9025,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html2canvas@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "html2canvas@npm:1.4.1"
|
||||
dependencies:
|
||||
css-line-break: ^2.1.0
|
||||
text-segmentation: ^1.0.3
|
||||
checksum: c134324af57f3262eecf982e436a4843fded3c6cf61954440ffd682527e4dd350e0c2fafd217c0b6f9a455fe345d0c67b4505689796ab160d4ca7c91c3766739
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-cache-semantics@npm:^4.1.1":
|
||||
version: 4.2.0
|
||||
resolution: "http-cache-semantics@npm:4.2.0"
|
||||
@@ -12819,6 +12857,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-pdf-from-image@npm:^0.3.6":
|
||||
version: 0.3.6
|
||||
resolution: "react-native-pdf-from-image@npm:0.3.6"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 2969aebd1b41b47e2b06869b40524de97e78c4a3d930b639921f53f2bd6bdf30a7e76a34794177ff02fed73f899825dce36621fd3b0b7722ae4dc38b35d9105f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-qrcode-svg@npm:^6.3.15":
|
||||
version: 6.3.15
|
||||
resolution: "react-native-qrcode-svg@npm:6.3.15"
|
||||
@@ -12931,6 +12979,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-view-shot@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "react-native-view-shot@npm:4.0.3"
|
||||
dependencies:
|
||||
html2canvas: ^1.4.1
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: d795849b5e2d1c75f66675aea073dae7d109b2b0a266d54f8c0ac8b30d4516dd83a5a7494fd2c44adf0e6e7286d3c29e9b025dbea3536332bf7165715f8a97de
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-vision-camera@npm:^4.6.4":
|
||||
version: 4.7.2
|
||||
resolution: "react-native-vision-camera@npm:4.7.2"
|
||||
@@ -14392,6 +14452,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"text-segmentation@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "text-segmentation@npm:1.0.3"
|
||||
dependencies:
|
||||
utrie: ^1.0.2
|
||||
checksum: 2e24632d59567c55ab49ac324815e2f7a8043e63e26b109636322ac3e30692cee8679a448fd5d0f0598a345f407afd0e34ba612e22524cf576d382d84058c013
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"text-table@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "text-table@npm:0.2.0"
|
||||
@@ -14903,6 +14972,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"utrie@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "utrie@npm:1.0.2"
|
||||
dependencies:
|
||||
base64-arraybuffer: ^1.0.2
|
||||
checksum: c96fbb7d4d8855a154327da0b18e39b7511cc70a7e4bcc3658e24f424bb884312d72b5ba777500b8858e34d365dc6b1a921dc5ca2f0d341182519c6b78e280a5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uuid@npm:^13.0.0":
|
||||
version: 13.0.0
|
||||
resolution: "uuid@npm:13.0.0"
|
||||
|
||||
Reference in New Issue
Block a user