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:
Blake Kaufman
2026-01-27 15:31:40 -05:00
committed by GitHub
parent 439b4b4dba
commit 0fcb8f7d34
23 changed files with 1172 additions and 327 deletions
@@ -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();
@@ -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,
},
});
@@ -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 });
}
};
+2 -2
View File
@@ -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 });
}
+268
View File
@@ -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;
}
}
+3 -8
View File
@@ -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) {
+2 -1
View File
@@ -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
+70
View File
@@ -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
+9 -3
View File
@@ -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",
+10 -5
View File
@@ -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 wont 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 isnt valid",
"paymentError": "Something went wrong while processing your payment. Please try again. If it still doesnt work, close the app and reopen it before trying again.",
"paymentFeeError": "We couldnt 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": {
+9 -3
View File
@@ -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",
+9 -3
View File
@@ -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",
+9 -3
View File
@@ -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": {
+9 -3
View File
@@ -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": {
+9 -3
View File
@@ -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": "Пожалуйста, не закрывайте приложение",
+9 -3
View File
@@ -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",
+3
View File
@@ -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",
+78
View File
@@ -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"