Lnurl recieve currency (#812)

* adding return key type to CustomSearchInput

* fix android crash

* adding receive currency type selector

* addding lnurlReceiveCurrency to masterinfoobject

* creating name and bio regex

* remove hardcoded strings

* fix receiveAddress not updating

* adding auto-focus to savings goal

* if using usd use zero amount invoice not lnurl since lnurl will go to usd not btc balance

* fix theme

* adding translations

* adding new edit flow

* fixing translation + dup keyboard
This commit is contained in:
Blake Kaufman
2026-05-05 14:26:13 -04:00
committed by GitHub
parent 5d81a089cb
commit 9045eb372e
24 changed files with 1409 additions and 347 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ android.enableJetifier=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
@@ -9,14 +9,17 @@ import {
CENTER,
COLORS,
CONTENT_KEYBOARD_OFFSET,
FONT,
EMAIL_REGEX,
SIZES,
VALID_USERNAME_REGEX,
VALID_NAME_BIO_REGEX,
} from '../../../../constants';
import { useNavigation } from '@react-navigation/native';
import { useEffect, useState, useRef } from 'react';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { useEffect, useState, useRef, useCallback } from 'react';
import { encriptMessage } from '../../../../functions/messaging/encodingAndDecodingMessages';
import { CustomKeyboardAvoidingView } from '../../../../functions/CustomElements';
import {
CustomKeyboardAvoidingView,
ThemeText,
} from '../../../../functions/CustomElements';
import { isValidUniqueName } from '../../../../../db';
import CustomButton from '../../../../functions/CustomElements/button';
import { useGlobalContactsInfo } from '../../../../../context-store/globalContacts';
@@ -39,6 +42,7 @@ import { useProfileImage } from './hooks/useProfileImage';
import EditProfileTextInput from './internalComponents/editProfileTextItems';
import { areImagesSame } from './utils/imageComparison';
import ThemeIcon from '../../../../functions/CustomElements/themeIcon';
import { useGlobalContextProvider } from '../../../../../context-store/context';
export default function EditMyProfilePage(props) {
const navigate = useNavigation();
@@ -61,6 +65,29 @@ export default function EditMyProfilePage(props) {
const myContact = globalContactsInformation.myProfile;
const isFirstTimeEditing = myContact.didEditProfile;
useFocusEffect(
useCallback(() => {
return () => {
if (isFirstTimeEditing || !isEditingMyProfile) return;
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
didEditProfile: true,
},
addedContacts: globalContactsInformation.addedContacts,
},
true,
);
};
}, [
isFirstTimeEditing,
globalContactsInformation,
toggleGlobalContactsInformation,
isEditingMyProfile,
]),
);
const selectedAddedContact = props.fromInitialAdd
? providedContact
: decodedAddedContacts.find(
@@ -96,21 +123,7 @@ export default function EditMyProfilePage(props) {
<CustomSettingsTopBar
shouldDismissKeyboard={true}
label={fromSettings ? t('contacts.editMyProfilePage.navTitle') : ''}
customBackFunction={() => {
if (!isFirstTimeEditing) {
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
didEditProfile: true,
},
addedContacts: globalContactsInformation.addedContacts,
},
true,
);
}
keyboardGoBack(navigate);
}}
customBackFunction={() => keyboardGoBack(navigate)}
iconNew="Trash2"
leftImageFunction={() =>
navigate.navigate('ConfirmActionPage', {
@@ -140,18 +153,17 @@ export default function EditMyProfilePage(props) {
);
}
// Extracted shared input fields component
// ─── Contact-mode input fields ───────────────────────────────────
function ProfileInputFields({
inputs,
changeInputText,
setIsKeyboardActive,
nameRef,
uniquenameRef,
bioRef,
receiveAddressRef,
isEditingMyProfile,
selectedAddedContact,
myContact,
theme,
darkModeType,
textInputColor,
@@ -160,6 +172,9 @@ function ProfileInputFields({
navigate,
t,
}) {
const hasLNURL = !isEditingMyProfile && selectedAddedContact?.isLNURL;
const bioIsLast = !isEditingMyProfile;
return (
<>
<EditProfileTextInput
@@ -176,9 +191,10 @@ function ProfileInputFields({
textInputColor={textInputColor}
textInputBackground={textInputBackground}
textColor={textColor}
showDivider={true}
/>
{selectedAddedContact?.isLNURL && (
{hasLNURL && (
<EditProfileTextInput
label={t('contacts.editMyProfilePage.lnurlInputDesc')}
placeholder={t('contacts.editMyProfilePage.lnurlInputPlaceholder')}
@@ -195,33 +211,7 @@ function ProfileInputFields({
textInputColor={textInputColor}
textInputBackground={textInputBackground}
textColor={textColor}
/>
)}
{isEditingMyProfile && (
<EditProfileTextInput
label={t('contacts.editMyProfilePage.uniqueNameInputDesc')}
placeholder={myContact.uniqueName}
value={inputs.uniquename}
onChangeText={text => changeInputText(text, 'uniquename')}
onFocus={() => setIsKeyboardActive(true)}
onBlur={() => setIsKeyboardActive(false)}
inputRef={uniquenameRef}
maxLength={30}
theme={theme}
darkModeType={darkModeType}
textInputColor={textInputColor}
textInputBackground={textInputBackground}
textColor={textColor}
showInfoIcon={true}
onInfoPress={() =>
navigate.navigate('InformationPopup', {
textContent: t(
'wallet.receivePages.editLNURLContact.informationMessage',
),
buttonText: t('constants.understandText'),
})
}
showDivider={true}
/>
)}
@@ -242,12 +232,130 @@ function ProfileInputFields({
textInputColor={textInputColor}
textInputBackground={textInputBackground}
textColor={textColor}
containerStyle={{ marginBottom: 10 }}
showDivider={!bioIsLast}
/>
</>
);
}
// ─── My profile nav rows ──────────────────────────────────────────────────────
function MyProfileRows({
myContact,
backgroundOffset,
textColor,
navigate,
masterInfoObject,
t,
}) {
const receiveCurrencyValue =
masterInfoObject.lnurlReceiveCurrency === 'usd'
? t('constants.dollars_upper')
: t('constants.bitcoin_upper');
return (
<View
style={[
styles.card,
{ backgroundColor: backgroundOffset, marginTop: 12 },
]}
>
{/* Name */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
navigate.navigate('EditProfileFieldPage', { fieldKey: 'name' })
}
style={styles.navRow}
>
<ThemeText
CustomNumberOfLines={1}
styles={styles.navRowValue}
content={t('contacts.editMyProfilePage.nameInputDesc')}
/>
<ThemeText
styles={styles.navRowLabel}
content={myContact?.name || t('contacts.splitBill.noName')}
CustomNumberOfLines={1}
CustomEllipsizeMode="tail"
/>
<ThemeIcon iconName="ChevronRight" size={16} />
</TouchableOpacity>
<View style={[styles.divider, { backgroundColor: textColor }]} />
{/* Username */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
navigate.navigate('EditProfileFieldPage', { fieldKey: 'uniquename' })
}
style={styles.navRow}
>
<ThemeText
CustomNumberOfLines={1}
styles={styles.navRowValue}
content={t('contacts.editMyProfilePage.uniqueNameInputDesc')}
/>
<ThemeText
styles={styles.navRowLabel}
content={myContact?.uniqueName || ''}
CustomNumberOfLines={1}
CustomEllipsizeMode="tail"
/>
<ThemeIcon iconName="ChevronRight" size={16} />
</TouchableOpacity>
<View style={[styles.divider, { backgroundColor: textColor }]} />
{/* Bio */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
navigate.navigate('EditProfileFieldPage', { fieldKey: 'bio' })
}
style={styles.navRow}
>
<ThemeText
CustomNumberOfLines={1}
styles={styles.navRowValue}
content={t('contacts.editMyProfilePage.bioInputDesc')}
/>
<ThemeText
styles={styles.navRowLabel}
content={myContact?.bio || t('constants.noBioSet')}
CustomNumberOfLines={1}
CustomEllipsizeMode="tail"
/>
<ThemeIcon iconName="ChevronRight" size={16} />
</TouchableOpacity>
<View style={[styles.divider, { backgroundColor: textColor }]} />
{/* Lightning Address */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
navigate.navigate('CustomHalfModal', {
wantedContent: 'lnurlReceiveCurrencySelect',
})
}
style={styles.navRow}
>
<ThemeText
CustomNumberOfLines={1}
styles={styles.navRowValue}
content={t('contacts.editMyProfilePage.lightningAddress')}
/>
<ThemeText styles={styles.navRowLabel} content={receiveCurrencyValue} />
<ThemeIcon iconName="ChevronRight" size={16} />
</TouchableOpacity>
</View>
);
}
// ─── InnerContent ─────────────────────────────────────────────────────────────
function InnerContent({
isEditingMyProfile,
selectedAddedContact,
@@ -260,6 +368,7 @@ function InnerContent({
const { cache, refreshCacheObject } = useImageCache();
const { backgroundOffset, textInputColor, textInputBackground, textColor } =
GetThemeColors();
const { masterInfoObject } = useGlobalContextProvider();
const {
decodedAddedContacts,
globalContactsInformation,
@@ -273,18 +382,13 @@ function InnerContent({
saveProfileImage,
} = useProfileImage();
// Contact-mode refs (unused in myProfile path but kept for contact path)
const nameRef = useRef(null);
const uniquenameRef = useRef(null);
const bioRef = useRef(null);
const receiveAddressRef = useRef(null);
const didCallImagePicker = useRef(null);
const myContact = globalContactsInformation.myProfile;
const myContactName = myContact?.name || '';
const myContactBio = myContact?.bio || '';
const myContactUniqueName = myContact?.uniqueName || '';
const isFirstTimeEditing = myContact.didEditProfile;
const selectedAddedContactName = selectedAddedContact?.name || '';
const selectedAddedContactBio = selectedAddedContact?.bio || '';
const selectedAddedContactUniqueName = selectedAddedContact?.uniqueName || '';
@@ -293,25 +397,12 @@ function InnerContent({
const [isSaving, setIsSaving] = useState(false);
const [inputs, setInputs] = useState(() => ({
name: isFirstTimeEditing
? isEditingMyProfile
? myContactName || ''
: selectedAddedContactName || ''
: '',
bio: isFirstTimeEditing
? isEditingMyProfile
? myContactBio || ''
: selectedAddedContactBio || ''
: '',
uniquename: isFirstTimeEditing
? isEditingMyProfile
? myContactUniqueName || ''
: selectedAddedContactUniqueName || ''
: '',
name: selectedAddedContactName || '',
bio: selectedAddedContactBio || '',
uniquename: selectedAddedContactUniqueName || '',
receiveAddress: selectedAddedContactReceiveAddress || '',
}));
// Remove the entire useEffect
const [tempImage, setTempImage] = useState({
uri: null,
comparison: null,
@@ -340,12 +431,9 @@ function InnerContent({
? !!myProfileImage?.localUri
: !!selectedAddedContactImage?.localUri;
// For contact path: full change detection. For myProfile: image only.
const hasChangedInfo = isEditingMyProfile
? myContactName !== inputs.name ||
myContactBio !== inputs.bio ||
myContactUniqueName !== inputs.uniquename ||
tempImage.uri ||
tempImage.shouldDelete
? tempImage.uri || tempImage.shouldDelete
: selectedAddedContactName !== inputs.name ||
selectedAddedContactBio !== inputs.bio ||
selectedAddedContactUniqueName !== inputs.uniquename ||
@@ -354,22 +442,6 @@ function InnerContent({
tempImage.uri ||
tempImage.shouldDelete;
console.log(
hasChangedInfo,
'has info changed',
selectedAddedContactName,
inputs.name,
selectedAddedContactBio,
inputs.bio,
selectedAddedContactUniqueName,
inputs.uniquename,
selectedAddedContactReceiveAddress,
inputs.receiveAddress,
fromInitialAdd,
tempImage.uri,
tempImage.shouldDelete,
);
const handleDeleteProfilePicture = () => {
setTempImage({
uri: null,
@@ -406,12 +478,10 @@ function InnerContent({
changeInputText,
setIsKeyboardActive,
nameRef,
uniquenameRef,
bioRef,
receiveAddressRef,
isEditingMyProfile,
selectedAddedContact,
myContact,
theme,
darkModeType,
textInputColor,
@@ -425,7 +495,9 @@ function InnerContent({
return (
<>
<View style={styles.hideProfileContainer}>
<ProfileInputFields {...inputFieldsProps} />
<View style={[styles.card, { backgroundColor: backgroundOffset }]}>
<ProfileInputFields {...inputFieldsProps} />
</View>
</View>
<CustomButton
buttonStyles={{
@@ -450,6 +522,116 @@ function InnerContent({
);
}
// ── myProfile path ──────────────────────────────────────────────────────────
if (isEditingMyProfile) {
return (
<View
style={[
styles.innerContainer,
fromSettings && { maxWidth: MAX_CONTENT_WIDTH, width: '100%' },
]}
>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{
flexGrow: 1,
alignItems: 'center',
width: fromSettings ? INSET_WINDOW_WIDTH : '100%',
...CENTER,
}}
keyboardShouldPersistTaps="handled"
>
{/* Profile image */}
<TouchableOpacity
activeOpacity={!isAddingImage ? 0.2 : 1}
onPress={async () => {
if (isAddingImage) return;
if (Keyboard.isVisible()) {
Keyboard.dismiss();
await new Promise(resolve => setTimeout(resolve, 250));
}
if (!hasImage) {
addProfilePicture();
return;
}
navigate.navigate('AddOrDeleteContactImage', {
addPhoto: () => addProfilePicture(),
deletePhoto: handleDeleteProfilePicture,
hasImage: hasImage,
});
}}
>
<View
style={[
styles.profileImage,
{ backgroundColor: backgroundOffset },
]}
>
{isAddingImage ? (
<FullLoadingScreen showText={false} />
) : (
<ContactProfileImage
updated={
tempImage.shouldDelete
? null
: tempImage.uri
? tempImage.comparison?.updated
: myProfileImage?.updated
}
uri={
tempImage.shouldDelete
? null
: tempImage.uri
? tempImage.comparison?.uri
: myProfileImage?.localUri
}
darkModeType={darkModeType}
theme={theme}
/>
)}
</View>
<View style={styles.selectFromPhotos}>
<ThemeIcon
colorOverride={COLORS.lightModeText}
size={20}
iconName={hasImage ? 'X' : 'Image'}
/>
</View>
</TouchableOpacity>
<ThemeText
styles={styles.sectionHeader}
content={t('contacts.editMyProfilePage.aboutYou')}
/>
<MyProfileRows
myContact={myContact}
backgroundOffset={backgroundOffset}
textColor={textColor}
navigate={navigate}
masterInfoObject={masterInfoObject}
t={t}
/>
<CustomButton
buttonStyles={{
width: '100%',
...CENTER,
marginTop: 'auto',
marginBottom: bottomPadding,
}}
actionFunction={saveChanges}
useLoading={isSaving}
textContent={
hasChangedInfo ? t('constants.save') : t('constants.back')
}
/>
</ScrollView>
</View>
);
}
// ── Contact path (unchanged) ────────────────────────────────────────────────
return (
<View
style={[
@@ -467,6 +649,7 @@ function InnerContent({
}}
keyboardShouldPersistTaps="handled"
>
{/* Profile image */}
<TouchableOpacity
activeOpacity={
(isEditingMyProfile || selectedAddedContact.isLNURL) &&
@@ -539,8 +722,11 @@ function InnerContent({
)}
</TouchableOpacity>
<ProfileInputFields {...inputFieldsProps} />
<View style={{ height: 40 }} />
{/* Unified settings card */}
<View style={[styles.card, { backgroundColor: backgroundOffset }]}>
<ProfileInputFields {...inputFieldsProps} />
</View>
<CustomButton
buttonStyles={{
width: '100%',
@@ -566,183 +752,155 @@ function InnerContent({
async function saveChanges() {
try {
if (isAddingImage) return;
setIsSaving(true);
// ── myProfile: image only ─────────────────────────────────────────────
if (isEditingMyProfile) {
if (tempImage.shouldDelete) {
await deleteProfilePicture(true, null);
} else if (tempImage.uri && tempImage.comparison) {
const areImagesTheSame = await areImagesSame(
tempImage.comparison?.uri,
myProfileImage?.localUri,
);
if (!areImagesTheSame) {
await saveProfileImage(tempImage, true, null);
}
}
keyboardGoBack(navigate);
return;
}
// ── Contact path (unchanged) ──────────────────────────────────────────
if (
inputs.name.length >= 30 ||
inputs.bio.length >= 150 ||
inputs.uniquename.length >= 30 ||
(selectedAddedContact?.isLNURL &&
inputs.receiveAddress.length >= 200) ||
isAddingImage
(selectedAddedContact?.isLNURL && inputs.receiveAddress.length >= 200)
)
return;
setIsSaving(true);
if (
!VALID_NAME_BIO_REGEX.test(inputs.name) ||
!VALID_NAME_BIO_REGEX.test(inputs.bio)
) {
navigate.navigate('ErrorScreen', {
errorMessage: t('contacts.editMyProfilePage.invalidCharactersError'),
});
return;
}
if (
selectedAddedContact?.isLNURL &&
!EMAIL_REGEX.test(inputs.receiveAddress.trim())
) {
navigate.navigate('ErrorScreen', {
errorMessage: t(
'contacts.editMyProfilePage.invalidReceiveAddressError',
),
});
return;
}
// delete or save new image
if (tempImage.shouldDelete) {
await deleteProfilePicture(isEditingMyProfile, selectedAddedContact);
await deleteProfilePicture(false, selectedAddedContact);
} else if (tempImage.uri && tempImage.comparison) {
const areImagesTheSame = await areImagesSame(
tempImage.comparison?.uri,
isEditingMyProfile
? myProfileImage?.localUri
: selectedAddedContactImage?.localUri,
selectedAddedContactImage?.localUri,
);
if (!areImagesTheSame) {
await saveProfileImage(
tempImage,
isEditingMyProfile,
selectedAddedContact,
);
await saveProfileImage(tempImage, false, selectedAddedContact);
}
}
const uniqueName =
isEditingMyProfile && !isFirstTimeEditing
? inputs.uniquename || myContact.uniqueName
: inputs.uniquename;
console.log(selectedAddedContact, 'tt', isEditingMyProfile);
if (isEditingMyProfile) {
if (
myContact?.bio === inputs.bio &&
myContact?.name === inputs.name &&
myContact?.uniqueName === inputs.uniquename &&
isFirstTimeEditing
) {
keyboardGoBack(navigate);
} else {
if (!VALID_USERNAME_REGEX.test(uniqueName)) {
navigate.navigate('ErrorScreen', {
errorMessage: t(
'contacts.editMyProfilePage.unqiueNameRegexError',
),
});
return;
}
if (myContact?.uniqueName != uniqueName) {
const isFreeUniqueName = await isValidUniqueName(
'blitzWalletUsers',
inputs.uniquename.trim(),
);
if (!isFreeUniqueName) {
navigate.navigate('ErrorScreen', {
errorMessage: t(
'contacts.editMyProfilePage.usernameAlreadyExistsError',
),
});
return;
}
}
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
name: inputs.name.trim(),
nameLower: inputs.name.trim().toLowerCase(),
bio: inputs.bio,
uniqueName: uniqueName.trim(),
uniqueNameLower: uniqueName.trim().toLowerCase(),
didEditProfile: true,
},
addedContacts: globalContactsInformation.addedContacts,
},
true,
);
keyboardGoBack(navigate);
if (fromInitialAdd) {
let tempContact = JSON.parse(JSON.stringify(selectedAddedContact));
tempContact.name = inputs.name.trim();
tempContact.nameLower = inputs.name.trim().toLowerCase();
tempContact.bio = inputs.bio;
tempContact.isAdded = true;
tempContact.unlookedTransactions = 0;
if (selectedAddedContact.isLNURL) {
tempContact.receiveAddress = inputs.receiveAddress;
}
let newAddedContacts = JSON.parse(JSON.stringify(decodedAddedContacts));
const isContactInAddedContacts = newAddedContacts.filter(
addedContact => addedContact.uuid === tempContact.uuid,
).length;
if (isContactInAddedContacts) {
newAddedContacts = newAddedContacts.map(addedContact => {
if (addedContact.uuid === tempContact.uuid) {
return {
...addedContact,
name: tempContact.name,
nameLower: tempContact.nameLower,
bio: tempContact.bio,
unlookedTransactions: 0,
isAdded: true,
};
} else return addedContact;
});
} else newAddedContacts.push(tempContact);
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
},
addedContacts: encriptMessage(
contactsPrivateKey,
publicKey,
JSON.stringify(newAddedContacts),
),
},
true,
);
return;
}
if (
selectedAddedContact?.bio === inputs.bio &&
selectedAddedContact?.name === inputs.name &&
selectedAddedContact?.receiveAddress === inputs.receiveAddress
) {
keyboardGoBack(navigate);
} else {
console.log(selectedAddedContact, 'testing');
if (fromInitialAdd) {
let tempContact = JSON.parse(JSON.stringify(selectedAddedContact));
tempContact.name = inputs.name.trim();
tempContact.nameLower = inputs.name.trim().toLowerCase();
tempContact.bio = inputs.bio;
tempContact.isAdded = true;
tempContact.unlookedTransactions = 0;
if (selectedAddedContact.isLNURL) {
tempContact.receiveAddress = inputs.receiveAddress;
}
let newAddedContacts = [...decodedAddedContacts];
const indexOfContact = decodedAddedContacts.findIndex(
obj => obj.uuid === selectedAddedContact.uuid,
);
let newAddedContacts = JSON.parse(
JSON.stringify(decodedAddedContacts),
);
const isContactInAddedContacts = newAddedContacts.filter(
addedContact => addedContact.uuid === tempContact.uuid,
).length;
let contact = newAddedContacts[indexOfContact];
if (isContactInAddedContacts) {
newAddedContacts = newAddedContacts.map(addedContact => {
if (addedContact.uuid === tempContact.uuid) {
return {
...addedContact,
name: tempContact.name,
nameLower: tempContact.nameLower,
bio: tempContact.bio,
unlookedTransactions: 0,
isAdded: true,
};
} else return addedContact;
});
} else newAddedContacts.push(tempContact);
console.log(tempContact, newAddedContacts);
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
},
addedContacts: encriptMessage(
contactsPrivateKey,
publicKey,
JSON.stringify(newAddedContacts),
),
},
true,
);
return;
}
contact['name'] = inputs.name.trim();
contact['nameLower'] = inputs.name.trim().toLowerCase();
contact['bio'] = inputs.bio.trim();
if (
selectedAddedContact?.bio === inputs.bio &&
selectedAddedContact?.name === inputs.name &&
selectedAddedContact?.receiveAddress === inputs.receiveAddress
)
keyboardGoBack(navigate);
else {
let newAddedContacts = [...decodedAddedContacts];
const indexOfContact = decodedAddedContacts.findIndex(
obj => obj.uuid === selectedAddedContact.uuid,
);
let contact = newAddedContacts[indexOfContact];
contact['name'] = inputs.name.trim();
contact['nameLower'] = inputs.name.trim().toLowerCase();
contact['bio'] = inputs.bio.trim();
if (
selectedAddedContact.isLNURL &&
selectedAddedContact?.receiveAddress !== inputs.receiveAddress
) {
contact['receiveAddress'] = inputs.receiveAddress.trim();
}
console.log(contact, newAddedContacts);
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
},
addedContacts: encriptMessage(
contactsPrivateKey,
publicKey,
JSON.stringify(newAddedContacts),
),
},
true,
);
keyboardGoBack(navigate);
selectedAddedContact.isLNURL &&
selectedAddedContact?.receiveAddress !== inputs.receiveAddress
) {
contact['receiveAddress'] = inputs.receiveAddress.trim();
}
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
},
addedContacts: encriptMessage(
contactsPrivateKey,
publicKey,
JSON.stringify(newAddedContacts),
),
},
true,
);
keyboardGoBack(navigate);
}
} catch (err) {
console.log(err);
@@ -762,6 +920,20 @@ const styles = StyleSheet.create({
innerContainer: {
flex: 1,
},
card: {
width: '100%',
borderRadius: 16,
overflow: 'hidden',
marginTop: 16,
marginBottom: 20,
},
sectionHeader: {
fontSize: SIZES.small,
includeFontPadding: false,
opacity: 0.55,
alignSelf: 'flex-start',
marginTop: 24,
},
selectFromPhotos: {
width: 30,
height: 30,
@@ -778,38 +950,50 @@ const styles = StyleSheet.create({
width: 150,
height: 150,
borderRadius: 125,
backgroundColor: 'red',
...CENTER,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 10,
overflow: 'hidden',
},
textInput: {
fontSize: SIZES.medium,
padding: 10,
fontFamily: FONT.Title_Regular,
includeFontPadding: false,
borderRadius: 8,
marginBottom: 10,
marginTop: 8,
},
textInputContainer: { width: '100%' },
textInputContainerDescriptionText: {
includeFontPadding: false,
},
usernameRow: {
navRow: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
paddingRight: 10,
gap: 5,
minHeight: 60,
},
navRowLabelLine: {
marginRight: 'auto',
flexShrink: 1,
},
navRowLabel: {
fontSize: SIZES.small,
includeFontPadding: false,
opacity: 0.55,
flexShrink: 1,
marginLeft: 10,
},
navRowValue: {
fontSize: SIZES.medium,
includeFontPadding: false,
marginRight: 'auto',
flexShrink: 1,
},
copyLinkRow: {
paddingVertical: 8,
},
infoIcon: {
width: 20,
height: 20,
marginLeft: 5,
copyLinkText: {
fontSize: SIZES.small,
includeFontPadding: false,
opacity: 0.55,
flex: 1,
marginRight: 4,
},
divider: {
height: StyleSheet.hairlineWidth,
marginHorizontal: 16,
opacity: 0.15,
},
});
@@ -295,6 +295,7 @@ export default function ExpandedContactsPage(props) {
selectedContact?.uniqueName,
selectedContact?.bio,
selectedContact?.isLNURL,
selectedContact?.receiveAddress,
imageData?.updated,
imageData?.localUri,
isConnectedToTheInternet,
@@ -1,5 +1,6 @@
import ContactsPage from './contactsPage';
import EditMyProfilePage from './editMyProfilePage';
import EditProfileFieldPage from './internalComponents/editProfileFieldPage';
import ExpandedAddContactsPage from './expandedAddContactsPage';
import ExpandedContactsPage from './expandedContactPage';
// import MyContactProfilePage from './myProfilePage';
@@ -13,6 +14,7 @@ import CreateSplitBill from './createSplitBill';
export {
ExpandedContactsPage,
EditMyProfilePage,
EditProfileFieldPage,
// MyContactProfilePage,
SendAndRequestPage,
ContactsPage,
@@ -0,0 +1,395 @@
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import { useEffect, useRef, useState } from 'react';
import { useNavigation } from '@react-navigation/native';
import {
COLORS,
FONT,
SIZES,
VALID_USERNAME_REGEX,
VALID_NAME_BIO_REGEX,
} from '../../../../../constants';
import { CENTER } from '../../../../../constants';
import {
CustomKeyboardAvoidingView,
ThemeText,
} from '../../../../../functions/CustomElements';
import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar';
import ThemeIcon from '../../../../../functions/CustomElements/themeIcon';
import CustomButton from '../../../../../functions/CustomElements/button';
import GetThemeColors from '../../../../../hooks/themeColors';
import { useGlobalThemeContext } from '../../../../../../context-store/theme';
import { useGlobalContactsInfo } from '../../../../../../context-store/globalContacts';
import { isValidUniqueName } from '../../../../../../db';
import CustomSearchInput from '../../../../../functions/CustomElements/searchInput';
import { keyboardGoBack } from '../../../../../functions/customNavigation';
import { useTranslation } from 'react-i18next';
const FIELD_CONFIG = {
name: {
title: 'contacts.editMyProfilePage.nameInputDesc',
description: 'contacts.editProfileFieldPage.nameDescription',
maxLength: 30,
multiline: false,
},
uniquename: {
title: 'contacts.editMyProfilePage.uniqueNameInputDesc',
description: 'contacts.editProfileFieldPage.uniquenameDescription',
maxLength: 30,
multiline: false,
},
bio: {
title: 'contacts.editMyProfilePage.bioInputDesc',
description: 'contacts.editProfileFieldPage.bioDescription',
maxLength: 150,
multiline: true,
},
};
// username validation states
const USERNAME_STATE = {
IDLE: 'idle',
CHECKING: 'checking',
AVAILABLE: 'available',
TAKEN: 'taken',
INVALID: 'invalid',
};
export default function EditProfileFieldPage(props) {
const fieldKey = props.route?.params?.fieldKey;
const config = FIELD_CONFIG[fieldKey] || FIELD_CONFIG.name;
const navigate = useNavigation();
const { theme, darkModeType } = useGlobalThemeContext();
const { textInputColor, textColor } = GetThemeColors();
const { globalContactsInformation, toggleGlobalContactsInformation } =
useGlobalContactsInfo();
const { t } = useTranslation();
const myContact = globalContactsInformation.myProfile;
const hasEdited = myContact?.didEditProfile || false;
const initialValue =
fieldKey === 'name'
? myContact?.name || ''
: fieldKey === 'uniquename'
? hasEdited
? myContact?.uniqueName || ''
: ''
: myContact?.bio || '';
const [value, setValue] = useState(initialValue);
const [usernameState, setUsernameState] = useState(USERNAME_STATE.IDLE);
const [isSaving, setIsSaving] = useState(false);
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
const debounceRef = useRef(null);
const isMountedRef = useRef(true);
const inputRef = useRef(null);
const isValidContent =
fieldKey === 'uniquename' || VALID_NAME_BIO_REGEX.test(value);
const didEdit = initialValue !== value;
useEffect(() => {
const timer = setTimeout(() => inputRef.current?.focus(), 300);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
useEffect(() => {
if (fieldKey !== 'uniquename') return;
if (debounceRef.current) clearTimeout(debounceRef.current);
const trimmed = value.trim();
if (!trimmed) {
setUsernameState(USERNAME_STATE.IDLE);
return;
}
if (!VALID_USERNAME_REGEX.test(trimmed)) {
setUsernameState(USERNAME_STATE.INVALID);
return;
}
if (trimmed.toLowerCase() === myContact?.uniqueName?.toLowerCase()) {
setUsernameState(USERNAME_STATE.AVAILABLE);
return;
}
setUsernameState(USERNAME_STATE.CHECKING);
debounceRef.current = setTimeout(async () => {
try {
const isFree = await isValidUniqueName('blitzWalletUsers', trimmed);
if (isMountedRef.current) {
setUsernameState(
isFree ? USERNAME_STATE.AVAILABLE : USERNAME_STATE.TAKEN,
);
}
} catch {
if (isMountedRef.current) setUsernameState(USERNAME_STATE.IDLE);
}
}, 600);
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [value, fieldKey]);
const canSave =
!isSaving &&
isValidContent &&
(fieldKey !== 'uniquename' || usernameState === USERNAME_STATE.AVAILABLE);
async function handleSave() {
if (!canSave) {
keyboardGoBack(navigate);
return;
}
const trimmed = value.trim();
const noChange =
fieldKey === 'name'
? trimmed === myContact?.name
: fieldKey === 'uniquename'
? trimmed === myContact?.uniqueName
: trimmed === myContact?.bio;
if (noChange) {
keyboardGoBack(navigate);
return;
}
setIsSaving(true);
try {
let profileUpdate = { ...myContact, didEditProfile: true };
if (fieldKey === 'name') {
profileUpdate.name = trimmed;
profileUpdate.nameLower = trimmed.toLowerCase();
} else if (fieldKey === 'uniquename') {
profileUpdate.uniqueName = trimmed;
profileUpdate.uniqueNameLower = trimmed.toLowerCase();
} else {
profileUpdate.bio = trimmed;
}
toggleGlobalContactsInformation(
{
myProfile: profileUpdate,
addedContacts: globalContactsInformation.addedContacts,
},
true,
);
keyboardGoBack(navigate);
} catch (err) {
console.log(err);
} finally {
setIsSaving(false);
}
}
const labelMap = {
name: t('contacts.editMyProfilePage.nameInputDesc'),
uniquename: t('contacts.editMyProfilePage.uniqueNameInputDesc'),
bio: t('contacts.editMyProfilePage.bioInputDesc'),
};
const fieldLabel = labelMap[fieldKey] || '';
const statusText =
fieldKey === 'uniquename'
? usernameState === USERNAME_STATE.TAKEN
? t('contacts.editMyProfilePage.usernameAlreadyExistsError')
: usernameState === USERNAME_STATE.INVALID
? t('contacts.editMyProfilePage.unqiueNameRegexError')
: `${value.length} / ${config.maxLength}`
: !isValidContent
? t('contacts.editMyProfilePage.invalidCharactersError')
: `${value.length} / ${config.maxLength}`;
const statusColor =
fieldKey === 'uniquename'
? usernameState === USERNAME_STATE.AVAILABLE ||
usernameState === USERNAME_STATE.CHECKING ||
usernameState === USERNAME_STATE.IDLE
? textColor
: theme && darkModeType
? textColor
: COLORS.cancelRed
: !isValidContent
? theme && darkModeType
? textColor
: COLORS.cancelRed
: textColor;
return (
<CustomKeyboardAvoidingView
useTouchableWithoutFeedback={true}
useStandardWidth={true}
useLocalPadding={true}
isKeyboardActive={isKeyboardActive}
>
<CustomSettingsTopBar label="" />
<View style={styles.content}>
<View style={styles.topSection}>
<ThemeText styles={styles.title} content={t(config.title)} />
{config.description && (
<ThemeText
styles={styles.description}
content={t(config.description)}
/>
)}
<ThemeText styles={styles.inputLabel} content={fieldLabel} />
<View style={styles.inputCard}>
<CustomSearchInput
textInputRef={inputRef}
inputText={value}
setInputText={setValue}
maxLength={config.maxLength}
textInputMultiline={config.multiline}
textAlignVertical={config.multiline ? 'top' : 'center'}
onBlurFunction={() => setIsKeyboardActive(false)}
onFocusFunction={() => setIsKeyboardActive(true)}
textInputStyles={{ paddingRight: 20 }}
placeholderText={
fieldKey === 'uniquename' && !hasEdited
? myContact?.uniqueName || ''
: undefined
}
/>
{fieldKey === 'uniquename' && (
<View style={styles.usernameIcon}>
{usernameState === USERNAME_STATE.CHECKING ? (
<ActivityIndicator size="small" color={textInputColor} />
) : usernameState === USERNAME_STATE.AVAILABLE ? (
<ThemeIcon
iconName="CircleCheck"
size={20}
colorOverride={
theme && darkModeType ? textInputColor : COLORS.nostrGreen
}
/>
) : usernameState === USERNAME_STATE.INVALID ||
usernameState === USERNAME_STATE.TAKEN ? (
<ThemeIcon
iconName="CircleAlert"
size={20}
colorOverride={
theme && darkModeType ? textInputColor : COLORS.cancelRed
}
/>
) : null}
</View>
)}
</View>
{statusText && (
<ThemeText
styles={[
styles.statusText,
{
color: statusColor,
opacity:
usernameState === USERNAME_STATE.TAKEN ||
usernameState === USERNAME_STATE.INVALID ||
!isValidContent
? 1
: 0.55,
},
]}
content={statusText}
/>
)}
</View>
<CustomButton
buttonStyles={{
width: '100%',
...CENTER,
}}
actionFunction={handleSave}
useLoading={isSaving}
textContent={
didEdit && canSave ? t('constants.save') : t('constants.back')
}
/>
</View>
</CustomKeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
content: {
flex: 1,
paddingHorizontal: 16,
justifyContent: 'space-between',
},
topSection: {
flex: 1,
},
title: {
fontSize: SIZES.large,
fontWeight: '500',
includeFontPadding: false,
marginTop: 10,
marginBottom: 8,
},
description: {
fontSize: SIZES.smedium,
includeFontPadding: false,
opacity: 0.6,
lineHeight: 22,
marginBottom: 16,
},
inputCard: {
borderRadius: 12,
marginTop: 8,
justifyContent: 'center',
},
inputLabel: {
width: '100%',
fontSize: SIZES.small,
includeFontPadding: false,
opacity: 0.55,
marginBottom: 4,
textTransform: 'uppercase',
marginTop: 16,
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
},
textInput: {
flex: 1,
fontSize: SIZES.medium,
fontFamily: FONT.Title_Regular,
includeFontPadding: false,
padding: 0,
},
multilineInput: {
minHeight: 60,
maxHeight: 120,
},
usernameIcon: {
width: 24,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
right: 10,
},
statusText: {
fontSize: SIZES.small,
includeFontPadding: false,
marginTop: 8,
},
});
@@ -48,6 +48,7 @@ export default function EditProfileTextInput({
showInfoIcon = false,
onInfoPress,
containerStyle,
showDivider = false,
}) {
const isOverLimit = value.length >= maxLength;
const characterCountColor = isOverLimit
@@ -60,32 +61,33 @@ export default function EditProfileTextInput({
? theme && darkModeType
? textInputColor
: COLORS.cancelRed
: textInputColor;
: textColor;
return (
<TouchableOpacity
style={[styles.textInputContainer, containerStyle]}
style={[styles.container, containerStyle]}
activeOpacity={1}
onPress={() => {
inputRef?.current?.focus();
}}
onPress={() => inputRef?.current?.focus()}
>
{showInfoIcon ? (
<TouchableOpacity onPress={onInfoPress} style={styles.usernameRow}>
<ThemeText
styles={styles.textInputContainerDescriptionText}
content={label}
/>
<View onPress={onInfoPress}>
<ThemeIcon size={20} styles={styles.infoIcon} iconName={'Info'} />
</View>
</TouchableOpacity>
) : (
<View style={styles.labelRow}>
<View style={styles.labelLeft}>
<ThemeText styles={styles.label} content={label} />
{showInfoIcon && (
<TouchableOpacity onPress={onInfoPress} style={styles.infoIconWrap}>
<ThemeIcon size={14} iconName="Info" />
</TouchableOpacity>
)}
</View>
<ThemeText
styles={styles.textInputContainerDescriptionText}
content={label}
styles={{
fontSize: SIZES.xSmall,
includeFontPadding: false,
color: characterCountColor,
opacity: isOverLimit ? 1 : 0.45,
}}
content={`${value.length} / ${maxLength}`}
/>
)}
</View>
<TextInput
keyboardAppearance={theme ? 'dark' : 'light'}
@@ -101,11 +103,7 @@ export default function EditProfileTextInput({
textAlignVertical={multiline ? 'top' : 'center'}
style={[
styles.textInput,
{
backgroundColor: textInputBackground,
color: inputTextColor,
marginTop: showInfoIcon ? 0 : 8,
},
{ color: inputTextColor },
multiline && {
minHeight: minHeight || 60,
maxHeight: maxHeight || 100,
@@ -117,45 +115,53 @@ export default function EditProfileTextInput({
onFocus={onFocus}
/>
<ThemeText
styles={{
textAlign: 'right',
color: characterCountColor,
}}
content={`${value.length} / ${maxLength}`}
/>
{showDivider && (
<View style={[styles.divider, { backgroundColor: textColor }]} />
)}
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
textInputContainer: {
container: {
width: '100%',
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 12,
},
textInputContainerDescriptionText: {
labelRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 4,
},
labelLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
label: {
fontSize: SIZES.small,
includeFontPadding: false,
opacity: 0.55,
},
infoIconWrap: {
marginLeft: 2,
opacity: 0.55,
},
textInput: {
fontSize: SIZES.medium,
paddingTop: 15,
paddingBottom: 15,
paddingLeft: 10,
paddingRight: 10,
fontFamily: FONT.Title_Regular,
includeFontPadding: false,
borderRadius: 8,
marginBottom: 10,
backgroundColor: 'transparent',
padding: 0,
},
usernameRow: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
paddingRight: 10,
paddingVertical: 8,
},
infoIcon: {
width: 20,
height: 20,
marginLeft: 5,
divider: {
position: 'absolute',
bottom: 0,
left: 16,
right: 16,
height: StyleSheet.hairlineWidth,
opacity: 0.15,
},
});
@@ -32,6 +32,7 @@ import { useToast } from '../../../../../context-store/toastManager';
import { copyToClipboard } from '../../../../functions';
import QrCodeWrapper from '../../../../functions/CustomElements/QrWrapper';
import { useAppStatus } from '../../../../../context-store/appStatus';
import { useGlobalContextProvider } from '../../../../../context-store/context';
// ─── LNURL Banner ────────────────────────────────────────────────────────────
@@ -127,12 +128,16 @@ const LNURLQROverlay = ({
visible,
onClose,
lnurlAddress,
navigate,
masterInfoObject,
theme,
darkModeType,
t,
}) => {
const { bottomPadding } = useGlobalInsets();
const overlayOpacity = useSharedValue(0);
const overlayTranslateX = useSharedValue(30);
const { textColor, backgroundColor, backgroundOffset } = GetThemeColors();
useEffect(() => {
overlayOpacity.value = withTiming(visible ? 1 : 0, { duration: 250 });
@@ -163,6 +168,37 @@ const LNURLQROverlay = ({
QRData={`${lnurlAddress}`}
/>
<View style={styles.selectionContainer}>
<TouchableOpacity
onPress={() =>
navigate.push('CustomHalfModal', {
wantedContent: 'lnurlReceiveCurrencySelect',
})
}
style={[
styles.currencyToggle,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
},
]}
>
<ThemeText
styles={styles.currencyToggleText}
content={
masterInfoObject.lnurlReceiveCurrency === 'btc'
? t('constants.bitcoin_upper')
: t('constants.dollars_upper')
}
/>
<ThemeIcon
colorOverride={textColor}
size={18}
iconName={'ChevronDown'}
/>
</TouchableOpacity>
</View>
{/* Back button */}
<CustomButton
buttonStyles={{
@@ -372,6 +408,7 @@ export default function HalfModalReceiveOptions({
const { cache } = useImageCache();
const { bottomPadding } = useGlobalInsets();
const { screenDimensions } = useAppStatus();
const { masterInfoObject } = useGlobalContextProvider();
const { decodedAddedContacts, contactsMessags, globalContactsInformation } =
useGlobalContacts();
const { t } = useTranslation();
@@ -605,7 +642,7 @@ export default function HalfModalReceiveOptions({
backgroundOffset={backgroundOffset}
textColor={textColor}
onQRPress={() => {
setContentHeight(500);
setContentHeight(600);
setShowLNURLQR(true);
}}
/>
@@ -738,6 +775,8 @@ export default function HalfModalReceiveOptions({
backgroundOffset={backgroundOffset}
textColor={textColor}
t={t}
navigate={navigate}
masterInfoObject={masterInfoObject}
setContentHeight={setContentHeight}
/>
@@ -821,7 +860,6 @@ const styles = StyleSheet.create({
// ── QR Overlay ──
qrOverlayContent: {
flex: 1,
justifyContent: 'space-between',
},
stickyHeaderContainer: {
@@ -979,4 +1017,25 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: 'space-between',
},
selectionContainer: {
width: INSET_WINDOW_WIDTH,
flex: 1,
marginVertical: 20,
...CENTER,
alignItems: 'center',
gap: 5,
},
currencyToggle: {
flexDirection: 'row',
alignItems: 'center',
gap: 2,
...CENTER,
minHeight: 40,
paddingHorizontal: 15,
borderRadius: 50,
},
currencyToggleText: {
includeFontPadding: false,
},
});
@@ -0,0 +1,217 @@
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import GetThemeColors from '../../../../hooks/themeColors';
import CheckMarkCircle from '../../../../functions/CustomElements/checkMarkCircle';
import { CENTER, ICONS } from '../../../../constants';
import { COLORS, INSET_WINDOW_WIDTH, SIZES } from '../../../../constants/theme';
import { ThemeText } from '../../../../functions/CustomElements';
import { useGlobalThemeContext } from '../../../../../context-store/theme';
import { useGlobalContextProvider } from '../../../../../context-store/context';
import { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import ThemeImage from '../../../../functions/CustomElements/themeImage';
import { useTranslation } from 'react-i18next';
import displayCorrectDenomination from '../../../../functions/displayCorrectDenomination';
import { useNodeContext } from '../../../../../context-store/nodeContext';
export default function LnurlReceiveCurrencySelect({
handleBackPressFunction,
setContentHeight,
}) {
const { masterInfoObject, toggleMasterInfoObject } =
useGlobalContextProvider();
const { fiatStats } = useNodeContext();
const { theme, darkModeType } = useGlobalThemeContext();
const { backgroundOffset, backgroundColor } = GetThemeColors();
const navigate = useNavigation();
const { t } = useTranslation();
useEffect(() => {
setContentHeight(500);
}, []);
const onSelect = currency => {
handleBackPressFunction(() => {
toggleMasterInfoObject({ lnurlReceiveCurrency: currency });
navigate.goBack();
});
};
const currentCurrency =
masterInfoObject.lnurlReceiveCurrency === 'usd' ? 'usd' : 'btc';
return (
<View style={styles.innerContainer}>
<ThemeText
styles={{ fontWeight: 500, fontSize: SIZES.large, marginBottom: 15 }}
content={t('contacts.remotePaymentCurrencySelect.title')}
/>
<View
style={[
styles.card,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
},
]}
>
<TouchableOpacity
onPress={() => onSelect('btc')}
style={styles.optionRow}
>
<View
style={[
styles.iconContainer,
{
backgroundColor:
theme && darkModeType
? backgroundOffset
: COLORS.bitcoinOrange,
},
]}
>
<ThemeImage
styles={{ width: 25, height: 25 }}
lightModeIcon={ICONS.bitcoinIcon}
darkModeIcon={ICONS.bitcoinIcon}
lightsOutIcon={ICONS.bitcoinIcon}
/>
</View>
<View style={styles.textContainer}>
<ThemeText
styles={styles.optionTitle}
content={t('constants.bitcoin_upper')}
/>
<ThemeText
styles={styles.optionSubtitle}
content={t(
'contacts.remotePaymentCurrencySelect.futurePaymentsMessage',
{
option: t('constants.bitcoin_upper'),
},
)}
/>
</View>
<CheckMarkCircle
isActive={currentCurrency === 'btc'}
containerSize={25}
switchDarkMode={theme && !darkModeType ? true : false}
/>
</TouchableOpacity>
<View style={styles.separator} />
<TouchableOpacity
onPress={() => onSelect('usd')}
style={styles.optionRow}
>
<View
style={[
styles.iconContainer,
{
backgroundColor:
theme && darkModeType ? backgroundOffset : COLORS.dollarGreen,
},
]}
>
<ThemeImage
styles={{ width: 25, height: 25 }}
lightModeIcon={ICONS.dollarIcon}
darkModeIcon={ICONS.dollarIcon}
lightsOutIcon={ICONS.dollarIcon}
/>
</View>
<View style={styles.textContainer}>
<ThemeText
styles={styles.optionTitle}
content={t('constants.dollars_upper')}
/>
<ThemeText
styles={styles.optionSubtitle}
content={t(
'contacts.remotePaymentCurrencySelect.futurePaymentsMessage',
{
option: t('constants.dollars_upper'),
},
)}
/>
</View>
<CheckMarkCircle
isActive={currentCurrency === 'usd'}
containerSize={25}
switchDarkMode={theme && !darkModeType ? true : false}
/>
</TouchableOpacity>
</View>
<ThemeText
styles={styles.footnote}
content={t('contacts.remotePaymentCurrencySelect.warningMessage', {
option: t('constants.dollars_upper'),
amount: displayCorrectDenomination({
amount: 2000,
masterInfoObject: {
...masterInfoObject,
userBalanceDenomination: 'sats',
},
fiatStats,
}),
})}
/>
</View>
);
}
const styles = StyleSheet.create({
innerContainer: {
flex: 1,
width: INSET_WINDOW_WIDTH,
...CENTER,
},
card: {
width: '100%',
borderRadius: 12,
paddingHorizontal: 15,
paddingVertical: 5,
},
optionRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 15,
gap: 10,
},
textContainer: {
flex: 1,
marginRight: 15,
},
iconContainer: {
width: 48,
height: 48,
borderRadius: 24,
justifyContent: 'center',
alignItems: 'center',
},
optionTitle: {
fontWeight: 500,
fontSize: SIZES.large,
includeFontPadding: false,
},
optionSubtitle: {
fontSize: SIZES.small,
opacity: 0.6,
marginTop: 3,
},
separator: {
width: '100%',
height: 1,
opacity: 0.1,
backgroundColor: 'white',
},
footnote: {
marginTop: 15,
fontSize: SIZES.small,
opacity: 0.5,
textAlign: 'center',
},
});
@@ -1,5 +1,5 @@
import { useNavigation } from '@react-navigation/native';
import { useState } from 'react';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, View } from 'react-native';
import {
@@ -8,7 +8,7 @@ import {
} from '../../../../functions/CustomElements';
import { CENTER, COLORS, SIZES } from '../../../../constants';
import CustomSearchInput from '../../../../functions/CustomElements/searchInput';
import { INSET_WINDOW_WIDTH, WINDOWWIDTH } from '../../../../constants/theme';
import { INSET_WINDOW_WIDTH } from '../../../../constants/theme';
import CustomButton from '../../../../functions/CustomElements/button';
import {
keyboardGoBack,
@@ -22,12 +22,20 @@ export default function SavingsGoalDescribe(props) {
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
const navigate = useNavigation();
const { t } = useTranslation();
const inputRef = useRef(null);
const emoji = props?.route?.params?.emoji || '🎯';
const [goalName, setGoalName] = useState('');
const { theme, darkModeType } = useGlobalThemeContext();
const { textColor } = GetThemeColors();
useFocusEffect(
useCallback(() => {
const timer = setTimeout(() => inputRef.current?.focus(), 300);
return () => clearTimeout(timer);
}, []),
);
const isOverLimit = goalName.length >= 50;
const characterCountColor = isOverLimit
? theme && darkModeType
@@ -54,6 +62,7 @@ export default function SavingsGoalDescribe(props) {
/>
<CustomSearchInput
textInputRef={inputRef}
containerStyles={styles.inputWrap}
placeholderText={t('savings.goalDescribe.placeholder')}
setInputText={setGoalName}
+2
View File
@@ -4,6 +4,7 @@ import CameraModal from './homeComponents/cameraModal';
import {
ContactsPage,
EditMyProfilePage,
EditProfileFieldPage,
ExpandedContactsPage,
// MyContactProfilePage,
SendAndRequestPage,
@@ -63,6 +64,7 @@ export {
HalfModalReceiveOptions,
ExpandedContactsPage,
EditMyProfilePage,
EditProfileFieldPage,
// MyContactProfilePage,
SendAndRequestPage,
ErrorScreen,
+2
View File
@@ -17,6 +17,7 @@ const IS_BLITZ_URL_REGEX =
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const VALID_USERNAME_REGEX = /^(?=.*\p{L})[\p{L}\p{N}_]+$/u;
const VALID_NAME_BIO_REGEX = /^[^<>{}`\\]*$/u;
const IS_SPARK_ID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -161,6 +162,7 @@ export {
MAX_CHANNEL_OPEN_FEE,
EMAIL_REGEX,
VALID_USERNAME_REGEX,
VALID_NAME_BIO_REGEX,
BLITZ_RECEIVE_FEE,
BLITZ_SEND_FEE,
AUTO_CHANNEL_REBALANCE_STORAGE_KEY,
@@ -72,6 +72,7 @@ import AddGiftQuantityHalfModal from '../../components/admin/homeComponents/gift
import SwapFlowHalfModal from '../../components/admin/homeComponents/swaps/swapFlowHalfModal';
import TxFilterHalfModal from '../../components/admin/homeComponents/homeLightning/txFilterHalfModal';
import PayLinkCurrencySelect from '../../components/admin/homeComponents/payLinks/components/payLinkCurrencySelect';
import LnurlReceiveCurrencySelect from '../../components/admin/homeComponents/receiveBitcoin/lnurlReceiveCurrencySelect';
import StablecoinAssetPickerHalfModal from './stablecoinAssetPickerHalfModal';
import RemoveBudgetHalfModal from '../../components/admin/homeComponents/analytics/removeBudgetHalfModal';
import BudgetWarningModal from '../../components/admin/homeComponents/sendBitcoin/components/nearBudgetLimitWarning';
@@ -579,6 +580,13 @@ export default function CustomHalfModal(props) {
setContentHeight={setContentHeight}
/>
);
case 'lnurlReceiveCurrencySelect':
return (
<LnurlReceiveCurrencySelect
handleBackPressFunction={handleBackPressFunction}
setContentHeight={setContentHeight}
/>
);
case 'stablecoinAssetPicker':
return (
<StablecoinAssetPickerHalfModal
@@ -26,6 +26,7 @@ export default function CustomSearchInput({
autoCapitalize = 'none',
editable = true,
autoFocus = false,
returnKeyType = 'default',
}) {
const { theme, darkModeType } = useGlobalThemeContext();
const { textInputColor, textInputBackground } = GetThemeColors();
@@ -139,6 +140,7 @@ export default function CustomSearchInput({
autoCapitalize={autoCapitalize}
autoCorrect={false}
editable={editable}
returnKeyType={returnKeyType}
/>
{buttonComponent && buttonComponent}
</View>
+2
View File
@@ -210,6 +210,7 @@ export default async function initializeUserSettingsFromHistory({
name: '',
pubkey: '',
};
const lnurlReceiveCurrency = blitzStoredData.lnurlReceiveCurrency || 'btc';
// let lnurlPubKey = blitzStoredData.lnurlPubKey;
@@ -404,6 +405,7 @@ export default async function initializeUserSettingsFromHistory({
tempObject['nextAccountDerivationIndex'] = nextAccountDerivationIndex;
tempObject['currentDerivedPoolIndex'] = currentDerivedPoolIndex;
tempObject['monthlyBudget'] = monthlyBudget;
tempObject['lnurlReceiveCurrency'] = lnurlReceiveCurrency;
// store in contacts context
tempObject['contacts'] = contacts;
+8 -5
View File
@@ -84,9 +84,10 @@ export default function ReceivePaymentHome(props) {
const [addressState, setAddressState] = useState({
selectedRecieveOption: selectedRecieveOption,
isReceivingSwap: false,
generatedAddress: isUsingAltAccount
? ''
: `${globalContactsInformation.myProfile.uniqueName}@blitzwalletapp.com`,
generatedAddress:
isUsingAltAccount || masterInfoObject.lnurlReceiveCurrency === 'usd'
? ''
: `${globalContactsInformation.myProfile.uniqueName}@blitzwalletapp.com`,
isGeneratingInvoice: false,
isHoldInvoice: false,
minMaxSwapAmount: {
@@ -175,7 +176,8 @@ export default function ReceivePaymentHome(props) {
!isUsingAltAccount &&
endReceiveType === 'BTC' &&
!paymentDescription &&
!addressState.isHoldInvoice
!addressState.isHoldInvoice &&
masterInfoObject.lnurlReceiveCurrency !== 'usd'
) {
setInitialSendAmount(0);
setAddressState(prev => ({
@@ -472,7 +474,8 @@ function QrCode(props) {
!isUsingAltAccount &&
endReceiveType === 'BTC' &&
!paymentDescription &&
!isHoldInvoice;
!isHoldInvoice &&
masterInfoObject.lnurlReceiveCurrency !== 'usd';
const qrOpacity = useSharedValue(addressState.generatedAddress ? 1 : 0);
const loadingOpacity = useSharedValue(isUsingLnurl ? 0 : 1);
+22 -1
View File
@@ -1246,7 +1246,18 @@
"usernameAlreadyExistsError": "Dieser Benutzername existiert bereits, bitte wählen Sie einen anderen.",
"unableToSaveError": "Profilbild konnte nicht gespeichert werden, bitte versuchen Sie es erneut.",
"deleteProfileImageError": "Profilbild konnte nicht gelöscht werden, bitte versuchen Sie es erneut.",
"deleteWarning": "Möchten Sie diesen Kontakt wirklich löschen? Nachrichten, die älter als eine Woche sind, können nicht wiederhergestellt werden."
"deleteWarning": "Möchten Sie diesen Kontakt wirklich löschen? Nachrichten, die älter als eine Woche sind, können nicht wiederhergestellt werden.",
"receiveCurrencyPillTitle": "Empfangswährung",
"receiveCurrencyPillDesc": "Legen Sie fest, wie Sie über Ihre Lightning-Adresse Zahlungen empfangen",
"invalidCharactersError": "Name und Bio dürfen die Zeichen < > { } ` \\ nicht enthalten",
"invalidReceiveAddressError": "Bitte geben Sie eine gültige Lightning-Adresse ein (z. B. benutzer@domain.com)",
"aboutYou": "Über dich",
"lightningAddress": "Lightning-Adresse"
},
"remotePaymentCurrencySelect": {
"title": "Empfangswährung",
"futurePaymentsMessage": "Alle zukünftigen Zahlungen werden in {{option}} empfangen",
"warningMessage": "Bei Auswahl von {{option}} gilt ein Mindestbetrag von {{amount}}. Kleinere Zahlungen werden in BTC empfangen."
},
"selectCurrencyToSend": {
"header": "Wie möchten Sie senden?"
@@ -1361,6 +1372,11 @@
"noDescription": "Bitte fügen Sie eine Beschreibung hinzu.",
"noContactsSelected": "Bitte wählen Sie mindestens einen weiteren Kontakt aus"
}
},
"editProfileFieldPage": {
"nameDescription": "Ihr Anzeigename ist der Name, unter dem andere Sie sehen. Er kann jederzeit geändert werden.",
"uniquenameDescription": "Wenn Sie Ihren Benutzernamen ändern, ändert sich auch Ihre Lightning-Adresse. Zahlungen an Ihren alten Benutzernamen gehen verloren.",
"bioDescription": "Ihre Bio ist eine kurze Beschreibung, damit andere Sie besser kennenlernen können."
}
},
"settings": {
@@ -2000,6 +2016,7 @@
"about": "Über uns",
"language": "Spracheinstellung",
"display currency": "Standardwährung",
"receive currency": "Lightning-Adresse",
"display options": "Anzeige & Layout",
"edit contact profile": "Kontaktprofil bearbeiten",
"view all swaps": "Netzwerk-Einstellungen",
@@ -2025,6 +2042,10 @@
"pools": "Sparpool",
"show seed phrase": "Seed-Phrase sichern"
},
"remotePayments": {
"lnAddress": "Lightning-Adresse",
"lnurlPageDesc": "Ihre Lightning-Adresse ermöglicht es jedem, Sie zu bezahlen, ohne dass eine Rechnung nötig ist."
},
"technicalTransactionDetails": {
"txHash": "Transaktions-Hash",
"paymentId": "Zahlungs-ID",
+22 -1
View File
@@ -1246,7 +1246,18 @@
"usernameAlreadyExistsError": "This username already exists, please choose another.",
"unableToSaveError": "Unable to save profile image, please try again.",
"deleteProfileImageError": "Unable to delete profile image, please try again.",
"deleteWarning": "Are you sure you want to delete this contact? Messages older than a week cannot be restored."
"deleteWarning": "Are you sure you want to delete this contact? Messages older than a week cannot be restored.",
"receiveCurrencyPillTitle": "Receive Currency",
"receiveCurrencyPillDesc": "Set how you receive via your Lightning address",
"invalidCharactersError": "Name and bio cannot contain the characters < > { } ` \\",
"invalidReceiveAddressError": "Please enter a valid Lightning address (e.g. user@domain.com)",
"aboutYou": "About you",
"lightningAddress": "Lightning Address"
},
"remotePaymentCurrencySelect": {
"title": "Receive currency",
"futurePaymentsMessage": "All future payments will be received as {{option}}",
"warningMessage": "When selecting {{option}}, theres a {{amount}} minimum. Smaller payments will be received in Bitcoin."
},
"selectCurrencyToSend": {
"header": "What do you want to send?"
@@ -1361,6 +1372,11 @@
"noDescription": "Please add a description for this split.",
"noContactsSelected": "Please select at least one other contact to continue"
}
},
"editProfileFieldPage": {
"nameDescription": "Your display name is how others see you. It can be changed anytime.",
"uniquenameDescription": "Changing your username also changes your Lightning Address. Payments to your old username will be lost.",
"bioDescription": "Your bio is a short description to help others get to know you."
}
},
"settings": {
@@ -2001,6 +2017,7 @@
"language": "Language",
"pools": "Pools",
"display currency": "Currency",
"receive currency": "Lightning Address",
"display options": "Display Options",
"edit contact profile": "Edit Contact Profile",
"view all swaps": "Network Transfers",
@@ -2025,6 +2042,10 @@
"blitzRestore": "Blitz Restore",
"point-of-sale": "Point-of-sale"
},
"remotePayments": {
"lnAddress": "Lightning address",
"lnurlPageDesc": "Your Lightning address lets anyone pay you with no invoice needed."
},
"technicalTransactionDetails": {
"txHash": "Transaction Hash",
"paymentId": "Payment ID",
+22 -1
View File
@@ -1246,7 +1246,18 @@
"usernameAlreadyExistsError": "Este nombre de usuario ya existe, elige otro.",
"unableToSaveError": "No se pudo guardar la imagen de perfil, intenta de nuevo.",
"deleteProfileImageError": "No se pudo eliminar la imagen de perfil, intenta de nuevo.",
"deleteWarning": "¿Seguro que deseas eliminar este contacto? Los mensajes de más de una semana no se pueden restaurar."
"deleteWarning": "¿Seguro que deseas eliminar este contacto? Los mensajes de más de una semana no se pueden restaurar.",
"receiveCurrencyPillTitle": "Moneda de recepción",
"receiveCurrencyPillDesc": "Configura cómo recibes pagos mediante tu dirección Lightning",
"invalidCharactersError": "El nombre y la biografía no pueden contener los caracteres < > { } ` \\",
"invalidReceiveAddressError": "Introduce una dirección Lightning válida (p. ej., usuario@dominio.com)",
"aboutYou": "Sobre ti",
"lightningAddress": "Dirección Lightning"
},
"remotePaymentCurrencySelect": {
"title": "Moneda de recepción",
"futurePaymentsMessage": "Todos los pagos futuros se recibirán en {{option}}",
"warningMessage": "Al seleccionar {{option}}, hay un mínimo de {{amount}}. Los pagos más pequeños se recibirán en Bitcoin."
},
"selectCurrencyToSend": {
"header": "¿Qué deseas enviar?"
@@ -1361,6 +1372,11 @@
"noDescription": "Agrega una descripción para esta división.",
"noContactsSelected": "Selecciona al menos un contacto para continuar"
}
},
"editProfileFieldPage": {
"nameDescription": "Tu nombre para mostrar es como te ven los demás. Puedes cambiarlo en cualquier momento.",
"uniquenameDescription": "Cambiar tu nombre de usuario también cambia tu dirección Lightning. Los pagos a tu nombre de usuario anterior se perderán.",
"bioDescription": "Tu biografía es una breve descripción para ayudar a otros a conocerte."
}
},
"settings": {
@@ -2000,6 +2016,7 @@
"about": "Acerca de",
"language": "idioma",
"display currency": "Moneda",
"receive currency": "Dirección Lightning",
"display options": "Opciones de visualización",
"edit contact profile": "Editar perfil de contacto",
"view all swaps": "Transferencias de red",
@@ -2025,6 +2042,10 @@
"pools": "Pools",
"show seed phrase": "Respaldar billetera"
},
"remotePayments": {
"lnAddress": "Dirección Lightning",
"lnurlPageDesc": "Tu dirección Lightning permite que cualquiera te pague sin necesidad de una factura."
},
"technicalTransactionDetails": {
"txHash": "Hash de transacción",
"paymentId": "ID de pago",
+22 -1
View File
@@ -1246,7 +1246,18 @@
"usernameAlreadyExistsError": "Ce nom d'utilisateur existe déjà, veuillez en choisir un autre.",
"unableToSaveError": "Impossible d'enregistrer l'image du profil, veuillez réessayer.",
"deleteProfileImageError": "Impossible de supprimer l'image du profil, veuillez réessayer.",
"deleteWarning": "Êtes-vous sûr de vouloir supprimer ce contact ? Les messages datant de plus d'une semaine ne peuvent pas être restaurés."
"deleteWarning": "Êtes-vous sûr de vouloir supprimer ce contact ? Les messages datant de plus d'une semaine ne peuvent pas être restaurés.",
"receiveCurrencyPillTitle": "Devise de réception",
"receiveCurrencyPillDesc": "Définissez comment vous recevez via votre adresse Lightning",
"invalidCharactersError": "Le nom et la bio ne peuvent pas contenir les caractères < > { } ` \\",
"invalidReceiveAddressError": "Veuillez saisir une adresse Lightning valide (p. ex. utilisateur@domaine.com)",
"aboutYou": "À propos de vous",
"lightningAddress": "Adresse Lightning"
},
"remotePaymentCurrencySelect": {
"title": "Devise de réception",
"futurePaymentsMessage": "Tous les paiements futurs seront reçus en {{option}}",
"warningMessage": "Lors de la sélection de {{option}}, un minimum de {{amount}} sapplique. Les paiements plus petits seront reçus en Bitcoin."
},
"selectCurrencyToSend": {
"header": "Que souhaitez-vous envoyer ?"
@@ -1361,6 +1372,11 @@
"noDescription": "Veuillez ajouter une description.",
"noContactsSelected": "Veuillez sélectionner au moins un contact"
}
},
"editProfileFieldPage": {
"nameDescription": "Votre nom daffichage est la façon dont les autres vous voient. Vous pouvez le modifier à tout moment.",
"uniquenameDescription": "Changer votre nom d'utilisateur change aussi votre adresse Lightning. Les paiements envoyés à votre ancien nom d'utilisateur seront perdus.",
"bioDescription": "Votre bio est une courte description qui aide les autres à mieux vous connaître."
}
},
"settings": {
@@ -2000,6 +2016,7 @@
"about": "A propos de",
"language": "Langue",
"display currency": "Monnaie",
"receive currency": "Adresse Lightning",
"display options": "Options d'affichage",
"edit contact profile": "Modifier le profil du contact",
"view all swaps": "Transferts de réseaux",
@@ -2025,6 +2042,10 @@
"pools": "Pools",
"show seed phrase": "Sauvegarder le portefeuille"
},
"remotePayments": {
"lnAddress": "Adresse Lightning",
"lnurlPageDesc": "Votre adresse Lightning permet à nimporte qui de vous payer sans facture."
},
"technicalTransactionDetails": {
"txHash": "Hachure de la transaction",
"paymentId": "Id de paiement",
+22 -1
View File
@@ -1246,7 +1246,18 @@
"usernameAlreadyExistsError": "Questo nome utente esiste già, scegli un altro.",
"unableToSaveError": "Impossibile salvare l'immagine del profilo, riprova.",
"deleteProfileImageError": "Impossibile eliminare l'immagine del profilo, riprova.",
"deleteWarning": "Sei sicuro di voler eliminare questo contatto? I messaggi più vecchi di una settimana non possono essere ripristinati."
"deleteWarning": "Sei sicuro di voler eliminare questo contatto? I messaggi più vecchi di una settimana non possono essere ripristinati.",
"receiveCurrencyPillTitle": "Valuta di ricezione",
"receiveCurrencyPillDesc": "Imposta come ricevere tramite il tuo indirizzo Lightning",
"invalidCharactersError": "Nome e bio non possono contenere i caratteri < > { } ` \\",
"invalidReceiveAddressError": "Inserisci un indirizzo Lightning valido (es. utente@dominio.com)",
"aboutYou": "Su di te",
"lightningAddress": "Indirizzo Lightning"
},
"remotePaymentCurrencySelect": {
"title": "Valuta di ricezione",
"futurePaymentsMessage": "Tutti i pagamenti futuri saranno ricevuti in {{option}}",
"warningMessage": "Se selezioni {{option}}, è previsto un minimo di {{amount}}. I pagamenti più piccoli saranno ricevuti in Bitcoin."
},
"selectCurrencyToSend": {
"header": "Cosa vuoi inviare?"
@@ -1361,6 +1372,11 @@
"noDescription": "Aggiungi una descrizione.",
"noContactsSelected": "Seleziona almeno un contatto"
}
},
"editProfileFieldPage": {
"nameDescription": "Il tuo nome visualizzato è il modo in cui gli altri ti vedono. Può essere modificato in qualsiasi momento.",
"uniquenameDescription": "Modificare il tuo username cambia anche il tuo indirizzo Lightning. I pagamenti al tuo vecchio username andranno persi.",
"bioDescription": "La tua bio è una breve descrizione per aiutare gli altri a conoscerti."
}
},
"settings": {
@@ -2000,6 +2016,7 @@
"about": "Informazioni",
"language": "Lingua",
"display currency": "Valuta",
"receive currency": "Indirizzo Lightning",
"display options": "Opzioni di Visualizzazione",
"edit contact profile": "Modifica Profilo Contatto",
"view all swaps": "Trasferimenti di rete",
@@ -2025,6 +2042,10 @@
"pools": "Pools",
"show seed phrase": "Backup portafoglio"
},
"remotePayments": {
"lnAddress": "Indirizzo Lightning",
"lnurlPageDesc": "Il tuo indirizzo Lightning consente a chiunque di pagarti senza bisogno di una fattura."
},
"technicalTransactionDetails": {
"txHash": "Hash Transazione",
"paymentId": "ID Pagamento",
+22 -1
View File
@@ -1246,7 +1246,18 @@
"usernameAlreadyExistsError": "Este nome de usuário já existe, escolha outro.",
"unableToSaveError": "Não foi possível salvar a imagem do perfil, tente novamente.",
"deleteProfileImageError": "Não foi possível excluir a imagem do perfil, tente novamente.",
"deleteWarning": "Tem certeza de que deseja excluir este contato? Mensagens com mais de uma semana não podem ser restauradas."
"deleteWarning": "Tem certeza de que deseja excluir este contato? Mensagens com mais de uma semana não podem ser restauradas.",
"receiveCurrencyPillTitle": "Moeda de recebimento",
"receiveCurrencyPillDesc": "Defina como você recebe pelo seu endereço Lightning",
"invalidCharactersError": "O nome e a bio não podem conter os caracteres < > { } ` \\",
"invalidReceiveAddressError": "Digite um endereço Lightning válido (ex.: usuario@dominio.com)",
"aboutYou": "Sobre você",
"lightningAddress": "Endereço Lightning"
},
"remotePaymentCurrencySelect": {
"title": "Moeda de recebimento",
"futurePaymentsMessage": "Todos os pagamentos futuros serão recebidos em {{option}}",
"warningMessage": "Ao selecionar {{option}}, há um mínimo de {{amount}}. Pagamentos menores serão recebidos em Bitcoin."
},
"selectCurrencyToSend": {
"header": "O que você deseja enviar?"
@@ -1361,6 +1372,11 @@
"noDescription": "Adicione uma descrição.",
"noContactsSelected": "Selecione pelo menos um contato"
}
},
"editProfileFieldPage": {
"nameDescription": "Seu nome de exibição é como as outras pessoas veem você. Ele pode ser alterado a qualquer momento.",
"uniquenameDescription": "Alterar seu nome de usuário também altera seu endereço Lightning. Pagamentos para seu nome de usuário antigo serão perdidos.",
"bioDescription": "Sua bio é uma breve descrição para ajudar outras pessoas a conhecer você."
}
},
"settings": {
@@ -2000,6 +2016,7 @@
"about": "Sobre",
"language": "Idioma",
"display currency": "Moeda de exibição",
"receive currency": "Endereço Lightning",
"display options": "Opções de tela",
"edit contact profile": "Editar perfil de contato",
"view all swaps": "Transferências de rede",
@@ -2025,6 +2042,10 @@
"pools": "Vaquinhas",
"show seed phrase": "Fazer backup da carteira"
},
"remotePayments": {
"lnAddress": "Endereço Lightning",
"lnurlPageDesc": "Seu endereço Lightning permite que qualquer pessoa pague você sem precisar de fatura."
},
"technicalTransactionDetails": {
"txHash": "Hash da Transação",
"paymentId": "ID",
+22 -1
View File
@@ -1247,7 +1247,18 @@
"usernameAlreadyExistsError": "Имя занято, выберите другое.",
"unableToSaveError": "Не удалось сохранить фото.",
"deleteProfileImageError": "Не удалось удалить фото.",
"deleteWarning": "Удалить контакт? Сообщения старше недели нельзя восстановить."
"deleteWarning": "Удалить контакт? Сообщения старше недели нельзя восстановить.",
"receiveCurrencyPillTitle": "Валюта получения",
"receiveCurrencyPillDesc": "Настройте, как вы будете получать платежи через адрес Молнии",
"invalidCharactersError": "Имя и био не могут содержать символы < > { } ` \\",
"invalidReceiveAddressError": "Введите корректный адрес Молнии (например, user@domain.com)",
"aboutYou": "О вас",
"lightningAddress": "Lightning-адрес"
},
"remotePaymentCurrencySelect": {
"title": "Валюта получения",
"futurePaymentsMessage": "Все будущие платежи будут получены в {{option}}",
"warningMessage": "При выборе {{option}} действует минимальная сумма {{amount}}. Более мелкие платежи будут получены в Bitcoin."
},
"selectCurrencyToSend": {
"header": "Что вы хотите отправить?"
@@ -1362,6 +1373,11 @@
"noDescription": "Добавьте описание.",
"noContactsSelected": "Выберите хотя бы один контакт"
}
},
"editProfileFieldPage": {
"nameDescription": "Ваше отображаемое имя видят другие пользователи. Его можно изменить в любой момент.",
"uniquenameDescription": "Изменение юзернейма также изменит ваш адрес Молнии. Платежи на ваш старый юзернейм будут потеряны.",
"bioDescription": "Ваше био — это короткое описание, которое помогает другим узнать вас лучше."
}
},
"settings": {
@@ -2001,6 +2017,7 @@
"about": "О приложении",
"language": "Язык",
"display currency": "Валюта",
"receive currency": "Адрес Молнии",
"display options": "Отображение",
"edit contact profile": "Редактировать профиль",
"view all swaps": "Сетевые переводы",
@@ -2026,6 +2043,10 @@
"pools": "Пулы",
"show seed phrase": "Резервное копирование кошелька"
},
"remotePayments": {
"lnAddress": "Адрес Молнии",
"lnurlPageDesc": "Ваш адрес Молнии позволяет любому отправить вам платеж без инвойса."
},
"technicalTransactionDetails": {
"txHash": "Хэш транзакции",
"paymentId": "ID платежа",
+22 -1
View File
@@ -1246,7 +1246,18 @@
"usernameAlreadyExistsError": "Detta användarnamn finns redan, vänligen välj ett annat.",
"unableToSaveError": "Det gick inte att spara profilbilden, försök igen.",
"deleteProfileImageError": "Det går inte att ta bort profilbilden, försök igen.",
"deleteWarning": "Är du säker på att du vill ta bort den här kontakten? Meddelanden som är äldre än en vecka kan inte återställas."
"deleteWarning": "Är du säker på att du vill ta bort den här kontakten? Meddelanden som är äldre än en vecka kan inte återställas.",
"receiveCurrencyPillTitle": "Mottagningsvaluta",
"receiveCurrencyPillDesc": "Ställ in hur du tar emot via din Lightning-adress",
"invalidCharactersError": "Namn och bio får inte innehålla tecknen < > { } ` \\",
"invalidReceiveAddressError": "Ange en giltig Lightning-adress (t.ex. användare@domän.com)",
"aboutYou": "Om dig",
"lightningAddress": "Lightning-adress"
},
"remotePaymentCurrencySelect": {
"title": "Mottagningsvaluta",
"futurePaymentsMessage": "Alla framtida betalningar kommer att tas emot i {{option}}",
"warningMessage": "När du väljer {{option}} finns ett minimum på {{amount}}. Mindre betalningar tas emot i Bitcoin."
},
"selectCurrencyToSend": {
"header": "Vad vill du skicka?"
@@ -1361,6 +1372,11 @@
"noDescription": "Lägg till en beskrivning.",
"noContactsSelected": "Välj minst en kontakt"
}
},
"editProfileFieldPage": {
"nameDescription": "Ditt visningsnamn är hur andra ser dig. Det kan ändras när som helst.",
"uniquenameDescription": "Om du ändrar ditt användarnamn ändras också din Lightning-adress. Betalningar till ditt gamla användarnamn går förlorade.",
"bioDescription": "Din bio är en kort beskrivning som hjälper andra att lära känna dig."
}
},
"settings": {
@@ -2000,6 +2016,7 @@
"about": "Om",
"language": "Språk",
"display currency": "Valuta",
"receive currency": "Lightning-adress",
"display options": "Alternativ för visning",
"edit contact profile": "Redigera kontaktprofil",
"view all swaps": "Nätverksöverföringar",
@@ -2025,6 +2042,10 @@
"pools": "Pooler",
"show seed phrase": "Säkerhetskopiera plånbok"
},
"remotePayments": {
"lnAddress": "Lightning-adress",
"lnurlPageDesc": "Din Lightning-adress gör att vem som helst kan betala dig utan att någon faktura behövs."
},
"technicalTransactionDetails": {
"txHash": "Transaktionens hash",
"paymentId": "Betalning Id",
+2
View File
@@ -29,6 +29,7 @@ import {
AddFriendsToSplit,
CreateSplitBill,
EditMyProfilePage,
EditProfileFieldPage,
ExpandedAddContactsPage,
ExpandedContactsPage,
// MyContactProfilePage,
@@ -153,6 +154,7 @@ const SLIDE_FROM_RIGHT_SCREENS = [
{ name: 'ReceiveBTC', component: ReceivePaymentHome },
// { name: 'MyContactProfilePage', component: MyContactProfilePage },
{ name: 'EditMyProfilePage', component: EditMyProfilePage },
{ name: 'EditProfileFieldPage', component: EditProfileFieldPage },
{ name: 'ExpandedAddContactsPage', component: ExpandedAddContactsPage },
{ name: 'SendAndRequestPage', component: SendAndRequestPage },
{ name: 'AddFriendsToSplit', component: AddFriendsToSplit },