diff --git a/App.tsx b/App.tsx index 50d8d0227..8159209dd 100644 --- a/App.tsx +++ b/App.tsx @@ -1,9 +1,9 @@ import { NavigationContainer } from '@react-navigation/native'; -import React from 'react'; +import React, { useEffect } from 'react'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SizeClassProvider } from './components/Context/SizeClassProvider'; import { SettingsProvider } from './components/Context/SettingsProvider'; -import { BlueDefaultTheme } from './components/themes'; +import { BlueCurrentTheme, BlueDarkTheme, BlueDefaultTheme } from './components/themes'; import MasterView from './navigation/MasterView'; import { navigationRef } from './NavigationService'; import { useLogger } from '@react-navigation/devtools'; @@ -11,22 +11,30 @@ import { StorageProvider } from './components/Context/StorageProvider'; import { initializeIndexer } from './modules/SilentPaymentIndexer'; import { initializeRustJsiBridge } from './modules/RustJsiBridge'; import { INDEXER_BASE_URL } from '@env'; +import { useColorScheme } from 'react-native'; + +if (!INDEXER_BASE_URL) throw new Error('INDEXER_BASE_URL is not set'); const App = () => { - initializeRustJsiBridge(); + const colorScheme = useColorScheme(); - if (!INDEXER_BASE_URL) throw new Error('INDEXER_BASE_URL is not set'); + useEffect(() => { + initializeRustJsiBridge(); + initializeIndexer({ + baseUrl: INDEXER_BASE_URL, + timeout: 100000, + }); + }, []); - initializeIndexer({ - baseUrl: INDEXER_BASE_URL, - timeout: 100000, // 100 seconds for blockchain scanning operations (increased for slower connections) - }); + useEffect(() => { + BlueCurrentTheme.updateColorScheme(); + }, [colorScheme]); useLogger(navigationRef); return ( - + diff --git a/android/app/src/main/assets/fonts/ClashGrotesk-Bold.otf b/android/app/src/main/assets/fonts/ClashGrotesk-Bold.otf new file mode 100644 index 000000000..2f9b90394 Binary files /dev/null and b/android/app/src/main/assets/fonts/ClashGrotesk-Bold.otf differ diff --git a/android/app/src/main/assets/fonts/ClashGrotesk-Medium.otf b/android/app/src/main/assets/fonts/ClashGrotesk-Medium.otf new file mode 100644 index 000000000..66df8cba2 Binary files /dev/null and b/android/app/src/main/assets/fonts/ClashGrotesk-Medium.otf differ diff --git a/android/app/src/main/assets/fonts/ClashGrotesk-Regular.otf b/android/app/src/main/assets/fonts/ClashGrotesk-Regular.otf new file mode 100644 index 000000000..d73876a58 Binary files /dev/null and b/android/app/src/main/assets/fonts/ClashGrotesk-Regular.otf differ diff --git a/android/app/src/main/assets/fonts/ClashGrotesk-Semibold.otf b/android/app/src/main/assets/fonts/ClashGrotesk-Semibold.otf new file mode 100644 index 000000000..9a3d54189 Binary files /dev/null and b/android/app/src/main/assets/fonts/ClashGrotesk-Semibold.otf differ diff --git a/android/app/src/main/assets/fonts/ClashGrotesk-Variable.ttf b/android/app/src/main/assets/fonts/ClashGrotesk-Variable.ttf new file mode 100644 index 000000000..3d66c39c7 Binary files /dev/null and b/android/app/src/main/assets/fonts/ClashGrotesk-Variable.ttf differ diff --git a/android/link-assets-manifest.json b/android/link-assets-manifest.json new file mode 100644 index 000000000..455650d76 --- /dev/null +++ b/android/link-assets-manifest.json @@ -0,0 +1,25 @@ +{ + "migIndex": 1, + "data": [ + { + "path": "assets/fonts/ClashGrotesk-Bold.otf", + "sha1": "7e0a4dfecaf6dc11b831bcff9dfdca4815ff6bd3" + }, + { + "path": "assets/fonts/ClashGrotesk-Medium.otf", + "sha1": "dacc45a62aeb471bf95e4b7ed5eb3c409f1829e3" + }, + { + "path": "assets/fonts/ClashGrotesk-Regular.otf", + "sha1": "6025c9ee7fc14cdb0544a00ad9a224da38abfe98" + }, + { + "path": "assets/fonts/ClashGrotesk-Semibold.otf", + "sha1": "d43b8e6d6ac119d9806f16ad7d62bf93caba82d5" + }, + { + "path": "assets/fonts/ClashGrotesk-Variable.ttf", + "sha1": "b4bc066de0bbd47f84baf5f90d9739e13819c4ac" + } + ] +} \ No newline at end of file diff --git a/assets/fonts/ClashGrotesk-Bold.otf b/assets/fonts/ClashGrotesk-Bold.otf new file mode 100644 index 000000000..2f9b90394 Binary files /dev/null and b/assets/fonts/ClashGrotesk-Bold.otf differ diff --git a/assets/fonts/ClashGrotesk-Medium.otf b/assets/fonts/ClashGrotesk-Medium.otf new file mode 100644 index 000000000..66df8cba2 Binary files /dev/null and b/assets/fonts/ClashGrotesk-Medium.otf differ diff --git a/assets/fonts/ClashGrotesk-Regular.otf b/assets/fonts/ClashGrotesk-Regular.otf new file mode 100644 index 000000000..d73876a58 Binary files /dev/null and b/assets/fonts/ClashGrotesk-Regular.otf differ diff --git a/assets/fonts/ClashGrotesk-Semibold.otf b/assets/fonts/ClashGrotesk-Semibold.otf new file mode 100644 index 000000000..9a3d54189 Binary files /dev/null and b/assets/fonts/ClashGrotesk-Semibold.otf differ diff --git a/assets/fonts/ClashGrotesk-Variable.ttf b/assets/fonts/ClashGrotesk-Variable.ttf new file mode 100644 index 000000000..3d66c39c7 Binary files /dev/null and b/assets/fonts/ClashGrotesk-Variable.ttf differ diff --git a/components/icons/PayArrowIcon.tsx b/components/icons/PayArrowIcon.tsx new file mode 100644 index 000000000..06dc9e11a --- /dev/null +++ b/components/icons/PayArrowIcon.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +interface PayArrowIconProps { + color?: string; + size?: number; +} + +const PayArrowIcon: React.FC = ({ color = 'white', size = 20 }) => ( + + + + +); + +export default PayArrowIcon; diff --git a/components/icons/QRScanIcon.tsx b/components/icons/QRScanIcon.tsx new file mode 100644 index 000000000..135b30f1a --- /dev/null +++ b/components/icons/QRScanIcon.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +interface QRScanIconProps { + color?: string; + size?: number; +} + +const QRScanIcon: React.FC = ({ color = '#754CE8', size = 24 }) => ( + + + + + + + + + + + + + + +); + +export default QRScanIcon; diff --git a/components/icons/ReceiveArrowIcon.tsx b/components/icons/ReceiveArrowIcon.tsx new file mode 100644 index 000000000..a52b5a024 --- /dev/null +++ b/components/icons/ReceiveArrowIcon.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +interface ReceiveArrowIconProps { + color?: string; + size?: number; +} + +const ReceiveArrowIcon: React.FC = ({ color = '#754CE8', size = 20 }) => ( + + + + +); + +export default ReceiveArrowIcon; diff --git a/components/icons/SearchIcon.tsx b/components/icons/SearchIcon.tsx new file mode 100644 index 000000000..8891e79eb --- /dev/null +++ b/components/icons/SearchIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import Svg, { Path, Rect } from 'react-native-svg'; + +interface SearchIconProps { + size?: number; + background?: string; + stroke?: string; +} + +const SearchIcon: React.FC = ({ size = 48, background = '#ffffff', stroke = '#754CE8' }) => ( + + + + + +); + +export default SearchIcon; diff --git a/components/icons/SettingsButton.tsx b/components/icons/SettingsButton.tsx index 77d5ae033..5c11da096 100644 --- a/components/icons/SettingsButton.tsx +++ b/components/icons/SettingsButton.tsx @@ -1,38 +1,40 @@ import React from 'react'; import { StyleSheet, TouchableOpacity } from 'react-native'; -import { Icon } from '@rneui/themed'; -import { useTheme } from '../themes'; +import Svg, { Circle, Path } from 'react-native-svg'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc from '../../loc'; -const SettingsButton = () => { - const { colors } = useTheme(); +interface SettingsButtonProps { + background?: string; + iconColor?: string; +} + +const SettingsButton: React.FC = ({ background = '#F6F7F9', iconColor = '#545454' }) => { const { navigate } = useExtendedNavigation(); - const onPress = () => { - navigate('Settings'); - }; return ( navigate('Settings')} + style={styles.button} > - + + + + ); }; export default SettingsButton; -const style = StyleSheet.create({ - buttonStyle: { - width: 30, - height: 30, - borderRadius: 15, - justifyContent: 'center', - alignContent: 'center', +const styles = StyleSheet.create({ + button: { + marginTop: 8, }, }); diff --git a/components/icons/ShieldReceiveIcon.tsx b/components/icons/ShieldReceiveIcon.tsx new file mode 100644 index 000000000..cdc07b28e --- /dev/null +++ b/components/icons/ShieldReceiveIcon.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +interface ShieldReceiveIconProps { + size?: number; + background?: string; + borderColor?: string; + accent?: string; +} + +const ShieldReceiveIcon: React.FC = ({ + size = 94, + background = '#FAF5FF', + borderColor = '#F3E8FF', + accent = '#754CE8', +}) => ( + + + + + + + + +); + +export default ShieldReceiveIcon; diff --git a/components/themes.ts b/components/themes.ts index f944affec..21b57348b 100644 --- a/components/themes.ts +++ b/components/themes.ts @@ -73,25 +73,42 @@ export const BlueDefaultTheme = { androidRippleColor: '#CCCCCC', primary: '#754CE8', secondary: '#472EBF', + receiveBtnBackground: '#754CE812', + bannerBackground: '#754CE80D', + payBtnDisabledBackground: '#00000052', + requestBtnTextColor: '#754CE8', + requestBtnBorderColor: '#754CE8', + payBtnTextColor: '#ffffff', + bannerBorderColor: '#E6E2FA', + scanBtnBorderColor: '#E6E2FA', + settingsBtnBackground: '#F6F7F9', + settingsBtnIconColor: '#545454', + searchIconBackground: '#ffffff', + searchIconStroke: '#754CE8', + shieldIconBackground: '#FAF5FF', + shieldIconBorder: '#F3E8FF', + shieldIconAccent: '#754CE8', + shareAddrBorderColor: '#E6E2FA', + shareAddrBackground: 'transparent', }, }; export type Theme = typeof BlueDefaultTheme; -const BlueDarkTheme: Theme = { +export const BlueDarkTheme: Theme = { ...DarkTheme, closeImage: require('../img/close-white.png'), barStyle: 'light-content', colors: { ...BlueDefaultTheme.colors, ...DarkTheme.colors, - customHeader: '#000000', + customHeader: '#121212', brandingColor: '#000000', borderTopColor: '#9aa0aa', - background: '#000000', + background: '#121212', foregroundColor: '#ffffff', buttonDisabledBackgroundColor: '#3A3A3C', - buttonBackgroundColor: '#FF9500', + buttonBackgroundColor: '#754CE8', buttonTextColor: '#ffffff', lightButton: 'rgba(255,255,255,.1)', buttonAlternativeTextColor: '#ffffff', @@ -130,6 +147,23 @@ const BlueDarkTheme: Theme = { receiveText: '#37C0A1', navigationBarColor: '#3A3A3C', androidRippleColor: '#444444', + receiveBtnBackground: '#110732', + bannerBackground: '#1D1A2B', + payBtnDisabledBackground: '#FFFFFF52', + requestBtnTextColor: '#8763EB', + requestBtnBorderColor: '#8763EB8F', + payBtnTextColor: '#0D0D0D', + bannerBorderColor: '#2D264F', + scanBtnBorderColor: '#241F3B', + settingsBtnBackground: '#141414', + settingsBtnIconColor: '#AAAAAA', + searchIconBackground: '#0D0D0D', + searchIconStroke: '#8763EB', + shieldIconBackground: '#1D1A2B', + shieldIconBorder: '#181818', + shieldIconAccent: '#8763EB', + shareAddrBorderColor: '#8763EB8F', + shareAddrBackground: '#1D1A2B', }, }; diff --git a/constants/fonts.ts b/constants/fonts.ts new file mode 100644 index 000000000..4950fbe62 --- /dev/null +++ b/constants/fonts.ts @@ -0,0 +1,14 @@ +/** + * Clash Grotesk font family names, as registered by the native font assets + * (see assets/fonts and react-native.config.js). On both iOS and Android the + * family name resolves to the font's PostScript name, so reference a specific + * weight via `fontFamily` and avoid relying on `fontWeight`. + */ +export const ClashFont = { + regular: 'ClashGrotesk-Regular', + medium: 'ClashGrotesk-Medium', + semibold: 'ClashGrotesk-Semibold', + bold: 'ClashGrotesk-Bold', +} as const; + +export type ClashFontWeight = keyof typeof ClashFont; diff --git a/ios/Shroud.xcodeproj/project.pbxproj b/ios/Shroud.xcodeproj/project.pbxproj index f6aa98d93..24abea642 100644 --- a/ios/Shroud.xcodeproj/project.pbxproj +++ b/ios/Shroud.xcodeproj/project.pbxproj @@ -103,6 +103,16 @@ B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC232D69FF8700327F3D /* EventEmitter.swift */; }; B4D899942DCAE67700B959AA /* CustomSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = B4D899932DCAE67700B959AA /* CustomSegmentedControl.m */; }; C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + BB6BD773CC6B4AD2B0BD4170 /* ClashGrotesk-Variable.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 189C35F318E44CF1B20C625B /* ClashGrotesk-Variable.ttf */; }; + B5A8AA8497DC47258347BF5B /* ClashGrotesk-Variable.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 189C35F318E44CF1B20C625B /* ClashGrotesk-Variable.ttf */; }; + 45819468BC854B9CB4405FCC /* ClashGrotesk-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 238DF672DF854575845E9164 /* ClashGrotesk-Regular.otf */; }; + B5D885395E12464A9B2B28E5 /* ClashGrotesk-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 238DF672DF854575845E9164 /* ClashGrotesk-Regular.otf */; }; + 913107CA6F474C138A1D8B50 /* ClashGrotesk-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = E414D35C332E44F5B5A9D392 /* ClashGrotesk-Semibold.otf */; }; + FEB7CA95911D492CBE170BD6 /* ClashGrotesk-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = E414D35C332E44F5B5A9D392 /* ClashGrotesk-Semibold.otf */; }; + A1C3FE97CC5642D496F51D31 /* ClashGrotesk-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 544DDA37AC6C419EB0F64843 /* ClashGrotesk-Bold.otf */; }; + E66F4161FFDC4D42AA305CE8 /* ClashGrotesk-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 544DDA37AC6C419EB0F64843 /* ClashGrotesk-Bold.otf */; }; + CAEBAFE1583E442CA8D5FD5D /* ClashGrotesk-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 14E5C86F5636474D9852EAE1 /* ClashGrotesk-Medium.otf */; }; + C3712141FADE42FC87D60AB4 /* ClashGrotesk-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 14E5C86F5636474D9852EAE1 /* ClashGrotesk-Medium.otf */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -260,6 +270,11 @@ FC63C7054F1C4FDFB7A830E5 /* libRCTPrivacySnapshot.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTPrivacySnapshot.a; sourceTree = ""; }; FC98DC24A81A463AB8B2E6B1 /* libRNImagePicker.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNImagePicker.a; sourceTree = ""; }; FD7977067E1A496F94D8B1B7 /* libRNDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNDeviceInfo.a; sourceTree = ""; }; + 189C35F318E44CF1B20C625B /* ClashGrotesk-Variable.ttf */ = {isa = PBXFileReference; name = "ClashGrotesk-Variable.ttf"; path = "../assets/fonts/ClashGrotesk-Variable.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + 238DF672DF854575845E9164 /* ClashGrotesk-Regular.otf */ = {isa = PBXFileReference; name = "ClashGrotesk-Regular.otf"; path = "../assets/fonts/ClashGrotesk-Regular.otf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + E414D35C332E44F5B5A9D392 /* ClashGrotesk-Semibold.otf */ = {isa = PBXFileReference; name = "ClashGrotesk-Semibold.otf"; path = "../assets/fonts/ClashGrotesk-Semibold.otf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + 544DDA37AC6C419EB0F64843 /* ClashGrotesk-Bold.otf */ = {isa = PBXFileReference; name = "ClashGrotesk-Bold.otf"; path = "../assets/fonts/ClashGrotesk-Bold.otf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + 14E5C86F5636474D9852EAE1 /* ClashGrotesk-Medium.otf */ = {isa = PBXFileReference; name = "ClashGrotesk-Medium.otf"; path = "../assets/fonts/ClashGrotesk-Medium.otf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -351,6 +366,11 @@ CA741BA794714D3F80251AC9 /* Ionicons.ttf */, E8E8CE89B3D142C6A8A56C34 /* Octicons.ttf */, CF4A4D7AAD974D67A2D62B3E /* MaterialIcons.ttf */, + 189C35F318E44CF1B20C625B /* ClashGrotesk-Variable.ttf */, + 238DF672DF854575845E9164 /* ClashGrotesk-Regular.otf */, + E414D35C332E44F5B5A9D392 /* ClashGrotesk-Semibold.otf */, + 544DDA37AC6C419EB0F64843 /* ClashGrotesk-Bold.otf */, + 14E5C86F5636474D9852EAE1 /* ClashGrotesk-Medium.otf */, ); name = Resources; sourceTree = ""; @@ -712,6 +732,11 @@ B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */, B41C2E562BB3DCB8000FE097 /* PrivacyInfo.xcprivacy in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + BB6BD773CC6B4AD2B0BD4170 /* ClashGrotesk-Variable.ttf in Resources */, + 45819468BC854B9CB4405FCC /* ClashGrotesk-Regular.otf in Resources */, + 913107CA6F474C138A1D8B50 /* ClashGrotesk-Semibold.otf in Resources */, + A1C3FE97CC5642D496F51D31 /* ClashGrotesk-Bold.otf in Resources */, + CAEBAFE1583E442CA8D5FD5D /* ClashGrotesk-Medium.otf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -723,6 +748,11 @@ B44034112BCC40A400162242 /* fiatUnits.json in Resources */, B4742E9B2CCDBE8300380EEE /* Localizable.xcstrings in Resources */, 6DD410B7266CAF5C0087DE03 /* Assets.xcassets in Resources */, + B5A8AA8497DC47258347BF5B /* ClashGrotesk-Variable.ttf in Resources */, + B5D885395E12464A9B2B28E5 /* ClashGrotesk-Regular.otf in Resources */, + FEB7CA95911D492CBE170BD6 /* ClashGrotesk-Semibold.otf in Resources */, + E66F4161FFDC4D42AA305CE8 /* ClashGrotesk-Bold.otf in Resources */, + C3712141FADE42FC87D60AB4 /* ClashGrotesk-Medium.otf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Shroud/Info.plist b/ios/Shroud/Info.plist index 458415853..0df6aa6bb 100644 --- a/ios/Shroud/Info.plist +++ b/ios/Shroud/Info.plist @@ -187,6 +187,11 @@ Ionicons.ttf MaterialIcons.ttf Octicons.ttf + ClashGrotesk-Variable.ttf + ClashGrotesk-Regular.otf + ClashGrotesk-Semibold.otf + ClashGrotesk-Bold.otf + ClashGrotesk-Medium.otf UIBackgroundModes diff --git a/ios/Widgets/Info.plist b/ios/Widgets/Info.plist index f50ceb791..e294bb0fe 100644 --- a/ios/Widgets/Info.plist +++ b/ios/Widgets/Info.plist @@ -64,5 +64,13 @@ + UIAppFonts + + ClashGrotesk-Variable.ttf + ClashGrotesk-Regular.otf + ClashGrotesk-Semibold.otf + ClashGrotesk-Bold.otf + ClashGrotesk-Medium.otf + diff --git a/ios/link-assets-manifest.json b/ios/link-assets-manifest.json new file mode 100644 index 000000000..455650d76 --- /dev/null +++ b/ios/link-assets-manifest.json @@ -0,0 +1,25 @@ +{ + "migIndex": 1, + "data": [ + { + "path": "assets/fonts/ClashGrotesk-Bold.otf", + "sha1": "7e0a4dfecaf6dc11b831bcff9dfdca4815ff6bd3" + }, + { + "path": "assets/fonts/ClashGrotesk-Medium.otf", + "sha1": "dacc45a62aeb471bf95e4b7ed5eb3c409f1829e3" + }, + { + "path": "assets/fonts/ClashGrotesk-Regular.otf", + "sha1": "6025c9ee7fc14cdb0544a00ad9a224da38abfe98" + }, + { + "path": "assets/fonts/ClashGrotesk-Semibold.otf", + "sha1": "d43b8e6d6ac119d9806f16ad7d62bf93caba82d5" + }, + { + "path": "assets/fonts/ClashGrotesk-Variable.ttf", + "sha1": "b4bc066de0bbd47f84baf5f90d9739e13819c4ac" + } + ] +} \ No newline at end of file diff --git a/loc/en.json b/loc/en.json index d0ed7db45..789a6c3d8 100644 --- a/loc/en.json +++ b/loc/en.json @@ -322,7 +322,14 @@ "xpub_title": "Wallet XPUB", "more_info": "More Info", "details_delete_wallet_error_message": "Something went wrong while deleting this wallet. You can try again, or delete it anyway.", - "details_delete_anyway": "Delete anyway" + "details_delete_anyway": "Delete anyway", + "no_transactions_title": "No transactions yet", + "no_transactions_subtitle": "Share your silent payment address to receive your first private payment.", + "share_address": "Share your address", + "zero_balance_toast_title": "You need bitcoin to Send", + "zero_balance_toast_subtitle": "Receive your first payment to get started", + "request_button": "Request", + "pay_button": "Pay" }, "total_balance_view": { "display_in_bitcoin": "Display in Bitcoin", diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 668d5fc86..02ac29f65 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -41,7 +41,10 @@ const DetailViewStackScreensStack = () => { const { wallets } = useStorage(); const { sizeClass } = useSizeClass(); const DetailButton = useMemo(() => , []); - const RightBarButtons = useMemo(() => , []); + const RightBarButtons = useMemo( + () => , + [theme.colors.settingsBtnBackground, theme.colors.settingsBtnIconColor], + ); const walletListScreenOptions = useMemo(() => { return { diff --git a/navigation/LazyLoadingIndicator.tsx b/navigation/LazyLoadingIndicator.tsx index 177df8631..2900cfe15 100644 --- a/navigation/LazyLoadingIndicator.tsx +++ b/navigation/LazyLoadingIndicator.tsx @@ -1,11 +1,15 @@ import React from 'react'; import { ActivityIndicator, StyleSheet, View } from 'react-native'; +import { useTheme } from '../components/themes'; -export const LazyLoadingIndicator = () => ( - - - -); +export const LazyLoadingIndicator = () => { + const { colors } = useTheme(); + return ( + + + + ); +}; const styles = StyleSheet.create({ root: { flex: 1, justifyContent: 'center', alignItems: 'center' }, diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 000000000..7fbb83fe9 --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,3 @@ +module.exports = { + assets: ['./assets/fonts'], +}; diff --git a/screen/UnlockWith.tsx b/screen/UnlockWith.tsx index a014fdb6d..95ed07deb 100644 --- a/screen/UnlockWith.tsx +++ b/screen/UnlockWith.tsx @@ -7,6 +7,7 @@ import SafeArea from '../components/SafeArea'; import { BiometricType, unlockWithBiometrics, useBiometrics } from '../hooks/useBiometrics'; import loc from '../loc'; import { useStorage } from '../hooks/context/useStorage'; +import { useTheme } from '../components/themes'; enum AuthType { Encrypted, @@ -50,6 +51,7 @@ function reducer(state: State, action: Action): State { } const UnlockWith: React.FC = () => { + const { colors } = useTheme(); const [state, dispatch] = useReducer(reducer, initialState); const isUnlockingWallets = useRef(false); const { setWalletsInitialized, isStorageEncrypted, startAndDecrypt } = useStorage(); @@ -130,7 +132,7 @@ const UnlockWith: React.FC = () => { const renderUnlockOptions = () => { if (state.isAuthenticating) { - return ; + return ; } else { switch (state.auth.type) { case AuthType.Biometrics: diff --git a/screen/wallets/WalletsList.tsx b/screen/wallets/WalletsList.tsx index 561006107..1c4c6ec95 100644 --- a/screen/wallets/WalletsList.tsx +++ b/screen/wallets/WalletsList.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useEffect, useReducer, useRef, useMemo } from 'react'; +import React, { useCallback, useEffect, useReducer, useRef, useMemo, useState } from 'react'; import { useFocusEffect } from '@react-navigation/native'; -import { Alert, findNodeHandle, InteractionManager, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Alert, Animated, InteractionManager, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import A from '../../modules/analytics'; import { getClipboardContent } from '../../modules/clipboard'; import { isDesktop } from '../../modules/environment'; @@ -9,11 +9,10 @@ import triggerHapticFeedback, { HapticFeedbackTypes } from '../../modules/haptic import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; import { ExtendedTransaction, Transaction, TWallet } from '../../class/wallets/types'; import presentAlert from '../../components/Alert'; -import { FButton, FContainer } from '../../components/FloatButtons'; import { useTheme } from '../../components/themes'; import { TransactionListItem } from '../../components/TransactionListItem'; import { useSizeClass, SizeClass } from '../../modules/sizeClass'; -import loc, { formatBalance } from '../../loc'; +import loc, { formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit } from '../../models/bitcoinUnits'; import ActionSheet from '../ActionSheet'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; @@ -23,7 +22,14 @@ import { useStorage } from '../../hooks/context/useStorage'; import { useSettings } from '../../hooks/context/useSettings'; import SafeAreaSectionList from '../../components/SafeAreaSectionList'; import { scanQrHelper } from '../../helpers/scan-qr.ts'; -import ScanIcon from '../../components/ScanIcon'; +import QRScanIcon from '../../components/icons/QRScanIcon'; +import { useHeaderHeight } from '@react-navigation/elements'; +import { ClashFont } from '../../constants/fonts'; +import { satoshiToLocalCurrency } from '../../modules/currency'; +import SearchIcon from '../../components/icons/SearchIcon'; +import ShieldReceiveIcon from '../../components/icons/ShieldReceiveIcon'; +import ReceiveArrowIcon from '../../components/icons/ReceiveArrowIcon'; +import PayArrowIcon from '../../components/icons/PayArrowIcon'; const WalletsListSections = { WALLET: 'WALLET', TRANSACTIONS: 'TRANSACTIONS' }; @@ -102,36 +108,86 @@ const WalletsList: React.FC = () => { const navigation = useExtendedNavigation(); const dataSource = getTransactions(undefined, Infinity); const walletsCount = useRef(wallets.length); - const walletActionButtonsRef = useRef(); + const headerHeight = useHeaderHeight(); + const [showZeroBalanceToast, setShowZeroBalanceToast] = useState(false); + const toastOpacity = useRef(new Animated.Value(0)).current; + const toastTimerRef = useRef>(); + + const dismissToast = useCallback(() => { + Animated.timing(toastOpacity, { toValue: 0, duration: 200, useNativeDriver: true }).start(() => + setShowZeroBalanceToast(false), + ); + if (toastTimerRef.current) clearTimeout(toastTimerRef.current); + }, [toastOpacity]); + + const triggerZeroBalanceToast = useCallback(() => { + setShowZeroBalanceToast(true); + Animated.timing(toastOpacity, { toValue: 1, duration: 200, useNativeDriver: true }).start(); + if (toastTimerRef.current) clearTimeout(toastTimerRef.current); + toastTimerRef.current = setTimeout(dismissToast, 4000); + }, [toastOpacity, dismissToast]); + + useEffect(() => { + return () => { + if (toastTimerRef.current) clearTimeout(toastTimerRef.current); + }; + }, []); - const stylesHook = StyleSheet.create({ + const stylesHook = useMemo(() => ({ listHeaderBack: { backgroundColor: colors.background, paddingTop: sizeClass === SizeClass.Large ? 8 : 0, }, - listHeaderText: { - color: colors.foregroundColor, - flexShrink: 1, - }, walletContainer: { backgroundColor: colors.background, - paddingHorizontal: 16, - paddingVertical: 8, }, - noWalletText: { - color: colors.foregroundColor, - fontSize: 18, - textAlign: 'center', - marginVertical: 20, + trackPaymentBg: { + backgroundColor: colors.bannerBackground, + borderColor: colors.bannerBorderColor, + }, + receiveBtnStyle: { + backgroundColor: colors.receiveBtnBackground, + borderColor: colors.requestBtnBorderColor, }, - balanceAmountText: { + scanBtnStyle: { + backgroundColor: colors.background, + borderColor: colors.scanBtnBorderColor, + borderWidth: 1, + }, + payBtnActive: { + backgroundColor: colors.buttonBackgroundColor, + }, + payBtnDisabled: { + backgroundColor: colors.payBtnDisabledBackground, + }, + cardStyle: { + backgroundColor: colors.elevated, + borderColor: colors.lightBorder, + }, + foregroundText: { color: colors.foregroundColor, - fontSize: 36, - fontWeight: 'bold', - marginBottom: 8, - textAlign: 'center', }, - }); + alternativeText: { + color: colors.alternativeTextColor, + }, + requestBtnLabel: { + color: colors.requestBtnTextColor, + }, + payBtnLabel: { + color: colors.payBtnTextColor, + }, + toastRequestBtn: { + backgroundColor: colors.buttonBackgroundColor, + }, + shareAddrStyle: { + borderWidth: 1, + borderColor: colors.shareAddrBorderColor, + backgroundColor: colors.shareAddrBackground, + }, + shareAddrText: { + color: colors.requestBtnTextColor, + }, + }), [colors, sizeClass]); const refreshWallets = useCallback( async (index: number | undefined, showLoadingIndicator = true, showUpdateStatusIndicator = false) => { @@ -225,33 +281,35 @@ const WalletsList: React.FC = () => { const wallet = wallets.length > 0 ? wallets[0] : null; return ( - - {`${loc.transactions.list_title}${' '}`} - {wallet && ( navigation.navigate('TrackPayment')} activeOpacity={0.7} testID="TrackPaymentBanner" > + + + - {loc.track_payment.banner_title} - - {loc.track_payment.banner_subtitle} - + {loc.track_payment.banner_title} + {loc.track_payment.banner_subtitle} - + )} + {dataSource.length > 0 && {loc.transactions.list_title}} ); - }, [stylesHook.listHeaderBack, stylesHook.listHeaderText, colors, navigation, wallets]); + }, [ + stylesHook.listHeaderBack, + stylesHook.trackPaymentBg, + stylesHook.foregroundText, + stylesHook.alternativeText, + navigation, + wallets, + dataSource.length, + ]); const renderTransactionListsRow = useCallback( (item: ExtendedTransaction) => ( @@ -271,8 +329,6 @@ const WalletsList: React.FC = () => { newWalletPreferredUnit = BitcoinUnit.SATS; break; case BitcoinUnit.SATS: - newWalletPreferredUnit = BitcoinUnit.LOCAL_CURRENCY; - break; default: newWalletPreferredUnit = BitcoinUnit.BTC; break; @@ -284,25 +340,56 @@ const WalletsList: React.FC = () => { const renderWalletItem = useCallback(() => { const wallet = wallets.length > 0 ? wallets[0] : null; + if (!wallet) return null; - if (!wallet) { - return ( - - {loc.wallets.list_empty_txs1} - - ); - } - - const balanceText = formatBalance(wallet.getBalance(), wallet.getPreferredBalanceUnit(), true); + const balance = wallet.getBalance(); + const preferredUnit = wallet.getPreferredBalanceUnit(); + const displayNum = String(formatBalanceWithoutSuffix(balance, preferredUnit, true)); + const fiatLine = `≈ ${satoshiToLocalCurrency(balance)}`; + const hasBalance = balance > 0; return ( - - - {balanceText} + + + + {displayNum} + {preferredUnit} + + {fiatLine} + + + + + {loc.wallets.request_button} + + + + + + + + + {loc.wallets.pay_button} + + ); - }, [wallets, stylesHook, changeWalletBalanceUnit]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [wallets, stylesHook, colors, changeWalletBalanceUnit]); const renderSectionItem = useCallback( (item: { section: any; item: ExtendedTransaction }) => { @@ -334,57 +421,38 @@ const WalletsList: React.FC = () => { [sizeClass, renderListHeaderComponent], ); - const renderSectionFooter = useCallback( - (section: { section: { key: any } }) => { - switch (section.section.key) { - case WalletsListSections.TRANSACTIONS: - if (dataSource.length === 0 && !isLoading) { - return ( - - {loc.wallets.list_empty_txs1} - - ); - } else { - return null; - } - default: - return null; - } - }, - [dataSource.length, isLoading], - ); - - const renderButtons = useCallback(() => { - if (wallets.length > 0) { - return ( - - - - - - } - text="" - widthRatio={0.01} - testID="HomeScreenScanButton" - /> - - - ); - } else { - return null; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [wallets.length]); + const renderEmptyCard = useCallback(() => { + if (dataSource.length !== 0 || isLoading) return null; + const wallet = wallets.length > 0 ? wallets[0] : null; + return ( + + + + + {loc.wallets.no_transactions_title} + {loc.wallets.no_transactions_subtitle} + {wallet && ( + navigation.navigate('ReceiveDetails', { walletID: wallet.getID(), address: '' })} + activeOpacity={0.7} + > + {loc.wallets.share_address} + + )} + + ); + }, [ + dataSource.length, + isLoading, + wallets, + navigation, + stylesHook.cardStyle, + stylesHook.foregroundText, + stylesHook.alternativeText, + stylesHook.shareAddrStyle, + stylesHook.shareAddrText, + ]); const sectionListKeyExtractor = useCallback((item: any, index: any) => { return `${item}${index}`; @@ -413,6 +481,11 @@ const WalletsList: React.FC = () => { } }, [navigation, wallets]); + const onZeroBalanceRequestPress = useCallback(() => { + dismissToast(); + onReceiveButtonPressed(); + }, [dismissToast, onReceiveButtonPressed]); + const pasteFromClipboard = useCallback(async () => { onBarScanned(await getClipboardContent()); }, [onBarScanned]); @@ -427,12 +500,6 @@ const WalletsList: React.FC = () => { const props = { title: loc.send.header, options, cancelButtonIndex: 0 }; - const anchor = findNodeHandle(walletActionButtonsRef.current); - - if (anchor) { - options.push(String(anchor)); - } - ActionSheet.showActionSheetWithOptions(props, buttonIndex => { switch (buttonIndex) { case 0: @@ -487,7 +554,7 @@ const WalletsList: React.FC = () => { }, [sizeClass, wallets.length]); const getItemLayout = useCallback( - (data: any, index: number) => { + (_data: any, index: number) => { const headerHeight = getSectionHeaderHeight(); if (sizeClass === SizeClass.Large) { @@ -527,20 +594,33 @@ const WalletsList: React.FC = () => { return ( <> + contentContainerStyle={styles.sectionListContent} renderItem={renderSectionItem} keyExtractor={sectionListKeyExtractor} renderSectionHeader={renderSectionHeader} initialNumToRender={10} - renderSectionFooter={renderSectionFooter} + ListFooterComponent={renderEmptyCard} + ListFooterComponentStyle={styles.emptyCardOuter} sections={sections} - floatingButtonHeight={70} maxToRenderPerBatch={10} updateCellsBatchingPeriod={50} getItemLayout={getItemLayout} ignoreTopInset={true} // Ignore top inset as the screen header already handles it {...refreshProps} /> - {renderButtons()} + {showZeroBalanceToast && ( + + + {loc.wallets.zero_balance_toast_title} + {loc.wallets.zero_balance_toast_subtitle} + + + {loc.wallets.request_button} + + + )} ); }; @@ -548,80 +628,190 @@ const WalletsList: React.FC = () => { export default WalletsList; const styles = StyleSheet.create({ + sectionListContent: { + flexGrow: 1, + }, listHeaderBack: { flexDirection: 'column', - alignItems: 'center', paddingHorizontal: 16, - minHeight: 56, + paddingTop: 8, }, - listHeaderText: { - fontWeight: 'bold', - fontSize: 24, - marginVertical: 16, - flexWrap: 'wrap', - textAlign: 'center', + walletSection: { + paddingHorizontal: 16, + paddingTop: 32, + paddingBottom: 8, + }, + balanceHeader: { + alignItems: 'center', + marginBottom: 20, }, - footerRoot: { - top: 80, - height: 160, - marginBottom: 80, + balanceRow: { + flexDirection: 'row', + alignItems: 'baseline', + justifyContent: 'center', }, - footerEmpty: { - fontSize: 18, - color: '#9aa0aa', + balanceNumber: { + fontSize: 64, + fontWeight: '400', textAlign: 'center', + fontFamily: ClashFont.regular, }, - walletContainer: { - paddingHorizontal: 16, - paddingVertical: 8, + balanceUnit: { + fontSize: 22, + fontWeight: '400', + marginLeft: 10, + fontFamily: ClashFont.regular, }, - noWalletText: { - fontSize: 18, + balanceFiat: { + fontSize: 15, textAlign: 'center', - marginVertical: 20, - fontWeight: '500', + marginTop: 6, + fontFamily: ClashFont.regular, }, - balanceHeader: { - paddingHorizontal: 16, - paddingVertical: 24, - paddingBottom: 40, - paddingTop: 40, + actionRow: { + flexDirection: 'row', alignItems: 'center', + gap: 10, + marginBottom: 16, }, - balanceAmount: { - fontSize: 36, - fontWeight: 'bold', - marginBottom: 8, - textAlign: 'center', - }, - scanIconContainer: { - width: 40, - height: 40, + actionBtnWide: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', justifyContent: 'center', + gap: 6, + height: 52, + borderRadius: 14, + borderWidth: 1, + }, + actionBtnNoBorder: { + borderWidth: 0, + }, + actionBtnSquare: { + width: 52, + height: 52, + borderRadius: 14, alignItems: 'center', + justifyContent: 'center', + }, + actionBtnLabel: { + fontSize: 15, + fontFamily: ClashFont.medium, }, trackPaymentBanner: { flexDirection: 'row', alignItems: 'center', - width: '100%', - marginTop: 8, - marginBottom: 8, - padding: 16, - borderRadius: 12, + borderRadius: 14, borderWidth: 1, + padding: 14, + marginBottom: 12, + }, + trackPaymentIconCircle: { + width: 38, + height: 38, + borderRadius: 19, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, }, trackPaymentBannerContent: { flex: 1, }, trackPaymentBannerTitle: { fontSize: 15, - fontWeight: '600', marginBottom: 2, + fontFamily: ClashFont.medium, }, trackPaymentBannerSubtitle: { fontSize: 13, + fontFamily: ClashFont.regular, }, trackPaymentChevron: { - fontSize: 22, + fontSize: 20, + fontFamily: ClashFont.regular, + }, + transactionsLabel: { + fontSize: 13, + marginBottom: 8, + marginTop: 4, + fontFamily: ClashFont.regular, + }, + emptyCardOuter: { + flex: 1, + }, + emptyCard: { + flex: 1, + margin: 16, + marginTop: 8, + borderRadius: 16, + borderWidth: 1, + padding: 32, + alignItems: 'center', + }, + emptyIconOuter: { + marginBottom: 20, + }, + emptyTitle: { + fontSize: 18, + textAlign: 'center', + marginBottom: 8, + fontFamily: ClashFont.medium, + }, + emptySubtitle: { + fontSize: 14, + textAlign: 'center', + lineHeight: 20, + marginBottom: 24, + paddingHorizontal: 8, + fontFamily: ClashFont.regular, + }, + shareAddressButton: { + borderRadius: 12, + paddingVertical: 14, + paddingHorizontal: 32, + width: '100%', + alignItems: 'center', + }, + shareAddressText: { + fontSize: 15, + fontFamily: ClashFont.medium, + }, + zeroBalanceToast: { + position: 'absolute', + left: 16, + right: 16, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 14, + borderWidth: 1, + padding: 16, + gap: 12, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.12, + shadowRadius: 8, + elevation: 4, + }, + zeroBalanceToastText: { + flex: 1, + }, + zeroBalanceToastTitle: { + fontSize: 15, + fontFamily: ClashFont.semibold, + marginBottom: 2, + }, + zeroBalanceToastSubtitle: { + fontSize: 13, + fontFamily: ClashFont.regular, + }, + zeroBalanceRequestText: { + fontSize: 15, + fontFamily: ClashFont.medium, + color: '#ffffff', + }, + toastRequestBtn: { + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 12, }, });