diff --git a/app.json b/app.json
index e4532d8..60ec8b8 100644
--- a/app.json
+++ b/app.json
@@ -2,7 +2,7 @@
"expo": {
"name": "Metanet",
"slug": "metanet-mobile",
- "version": "0.3.0",
+ "version": "0.3.2",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "metanet",
diff --git a/app/auth/password.tsx b/app/auth/password.tsx
index b41f6e8..dfdd1c8 100644
--- a/app/auth/password.tsx
+++ b/app/auth/password.tsx
@@ -114,9 +114,9 @@ export default function PasswordScreen() {
disabled={!isButtonEnabled() || loading}
>
{loading ? (
-
+
) : (
- {t('continue')}
+ {t('continue')}
)}
diff --git a/app/auth/phone.tsx b/app/auth/phone.tsx
index da8d575..3aa7cf9 100644
--- a/app/auth/phone.tsx
+++ b/app/auth/phone.tsx
@@ -199,9 +199,9 @@ export default function PhoneScreen() {
disabled={!isValidPhoneNumber() || loading}
>
{loading ? (
-
+
) : (
- {t('continue')}
+ {t('continue')}
)}
diff --git a/app/browser.tsx b/app/browser.tsx
index 898b478..0076792 100644
--- a/app/browser.tsx
+++ b/app/browser.tsx
@@ -15,7 +15,6 @@ import {
Keyboard,
TouchableWithoutFeedback,
KeyboardAvoidingView,
- LayoutAnimation,
ScrollView,
Modal as RNModal,
BackHandler,
@@ -34,7 +33,6 @@ import {
} from 'react-native-gesture-handler'
import { TabView, SceneMap } from 'react-native-tab-view'
import Fuse from 'fuse.js'
-import * as Linking from 'expo-linking'
import { Ionicons } from '@expo/vector-icons'
import { observer } from 'mobx-react-lite'
import { router } from 'expo-router'
@@ -349,6 +347,7 @@ function Browser() {
const { manifest, fetchManifest, getStartUrl, shouldRedirectToStartUrl } = useWebAppManifest()
const [showBalance, setShowBalance] = useState(false)
const [isFullscreen, setIsFullscreen] = useState(false)
+ const activeCameraStreams = useRef>(new Set())
// Safety check - if somehow activeTab is null, force create a new tab
// This is done after all hooks to avoid violating Rules of Hooks
@@ -764,6 +763,59 @@ function Browser() {
// === 1. Injected JS ============================================
const injectedJavaScript = useMemo(
() => `
+ // Camera access polyfill - provides mock streams to prevent WKWebView camera access
+ (function() {
+ if (!navigator.mediaDevices) return;
+
+ const originalGetUserMedia = navigator.mediaDevices.getUserMedia?.bind(navigator.mediaDevices);
+
+ function createMockMediaStream() {
+ const mockTrack = {
+ id: 'mock-video-' + Date.now(),
+ kind: 'video',
+ label: 'React Native Camera',
+ enabled: true,
+ muted: false,
+ readyState: 'live',
+ stop() { this.readyState = 'ended'; },
+ addEventListener() {},
+ removeEventListener() {},
+ getSettings: () => ({ width: 640, height: 480, frameRate: 30 })
+ };
+
+ return {
+ id: 'mock-stream-' + Date.now(),
+ active: true,
+ getTracks: () => [mockTrack],
+ getVideoTracks: () => [mockTrack],
+ getAudioTracks: () => [],
+ addEventListener() {},
+ removeEventListener() {}
+ };
+ }
+
+ navigator.mediaDevices.getUserMedia = function(constraints) {
+ const hasVideo = constraints?.video === true ||
+ (typeof constraints?.video === 'object' && constraints.video);
+
+ if (hasVideo) {
+ // Notify React Native of camera request
+ window.ReactNativeWebView?.postMessage(JSON.stringify({
+ type: 'CAMERA_REQUEST',
+ constraints
+ }));
+
+ // Return mock stream immediately to prevent native camera
+ return Promise.resolve(createMockMediaStream());
+ }
+
+ // Allow audio-only requests through original implementation
+ return originalGetUserMedia ?
+ originalGetUserMedia(constraints) :
+ Promise.reject(new Error('Media not supported'));
+ };
+ })();
+
// Push Notification API polyfill
(function() {
// Check if Notification API already exists
@@ -942,6 +994,103 @@ function Browser() {
} catch (e) {}
});
+ // Completely replace getUserMedia to prevent WKWebView camera access
+ if (navigator.mediaDevices) {
+ // Store original for potential fallback, but never use it for video
+ const originalGetUserMedia = navigator.mediaDevices.getUserMedia?.bind(navigator.mediaDevices);
+
+ // Completely override getUserMedia - never call original for video constraints
+ navigator.mediaDevices.getUserMedia = function(constraints) {
+ console.log('[WebView] getUserMedia intercepted:', constraints);
+
+ // Check if requesting video - if so, handle in React Native
+ const hasVideo = constraints && (constraints.video === true ||
+ (typeof constraints.video === 'object' && constraints.video !== false));
+
+ if (hasVideo) {
+ console.log('[WebView] Video requested - handling in React Native');
+ // Send request to native - handle camera completely in React Native
+ window.ReactNativeWebView?.postMessage(JSON.stringify({
+ type: 'CAMERA_REQUEST',
+ constraints: constraints
+ }));
+
+ return new Promise((resolve, reject) => {
+ const handler = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ if (data.type === 'CAMERA_RESPONSE') {
+ window.removeEventListener('message', handler);
+ if (data.success) {
+ // Create a more complete mock MediaStream
+ const mockVideoTrack = {
+ id: 'mock-video-track-' + Date.now(),
+ kind: 'video',
+ label: 'React Native Camera',
+ enabled: true,
+ muted: false,
+ readyState: 'live',
+ stop: () => console.log('[WebView] Mock video track stopped'),
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ dispatchEvent: () => false,
+ getSettings: () => ({ width: 640, height: 480, frameRate: 30 }),
+ getCapabilities: () => ({ width: { min: 320, max: 1920 }, height: { min: 240, max: 1080 } }),
+ getConstraints: () => constraints.video || {}
+ };
+
+ const mockStream = {
+ id: 'mock-stream-' + Date.now(),
+ active: true,
+ getTracks: () => [mockVideoTrack],
+ getVideoTracks: () => [mockVideoTrack],
+ getAudioTracks: () => [],
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ dispatchEvent: () => false,
+ addTrack: () => {},
+ removeTrack: () => {},
+ clone: () => mockStream
+ };
+ console.log('[WebView] Resolving with mock stream:', mockStream);
+ resolve(mockStream);
+ } else {
+ reject(new Error(data.error || 'Camera access denied'));
+ }
+ }
+ } catch (e) {
+ reject(e);
+ }
+ };
+ window.addEventListener('message', handler);
+
+ // Timeout after 10 seconds
+ setTimeout(() => {
+ window.removeEventListener('message', handler);
+ reject(new Error('Camera request timeout'));
+ }, 10000);
+ });
+ } else if (constraints && constraints.audio && !hasVideo) {
+ // Audio-only requests can use original implementation
+ console.log('[WebView] Audio-only request - using original getUserMedia');
+ return originalGetUserMedia ? originalGetUserMedia(constraints) :
+ Promise.reject(new Error('Audio not supported'));
+ } else {
+ // No media requested
+ return Promise.reject(new Error('No media constraints specified'));
+ }
+ };
+
+ // Also override the deprecated navigator.getUserMedia if it exists
+ if (navigator.getUserMedia) {
+ navigator.getUserMedia = function(constraints, success, error) {
+ navigator.mediaDevices.getUserMedia(constraints)
+ .then(success)
+ .catch(error);
+ };
+ }
+ }
+
// Console logging
const originalLog = console.log;
const originalWarn = console.warn;
@@ -1143,6 +1292,47 @@ function Browser() {
return
}
+ // Handle camera requests
+ if (msg.type === 'CAMERA_REQUEST') {
+ console.log('Camera access requested by website:', msg.constraints)
+
+ // Track active camera stream for this tab
+ activeCameraStreams.current.add(activeTab.id.toString())
+
+ // Handle camera request entirely in React Native to avoid WKWebView camera issues
+ // For now, just grant permission - actual camera implementation would go here
+ try {
+ // Here you would implement actual React Native camera handling
+ // For now, we'll just grant permission and return mock stream
+ console.log('Granting camera permission - camera handled by React Native')
+
+ if (activeTab.webviewRef?.current) {
+ activeTab.webviewRef.current.injectJavaScript(`
+ window.dispatchEvent(new MessageEvent('message', {
+ data: JSON.stringify({
+ type: 'CAMERA_RESPONSE',
+ success: true
+ })
+ }));
+ `)
+ }
+ } catch (error) {
+ console.error('Camera permission error:', error)
+ if (activeTab.webviewRef?.current) {
+ activeTab.webviewRef.current.injectJavaScript(`
+ window.dispatchEvent(new MessageEvent('message', {
+ data: JSON.stringify({
+ type: 'CAMERA_RESPONSE',
+ success: false,
+ error: 'Camera permission denied'
+ })
+ }));
+ `)
+ }
+ }
+ return
+ }
+
// Handle notification permission request
if (msg.type === 'REQUEST_NOTIFICATION_PERMISSION') {
const permission = await handleNotificationPermissionRequest(activeTab.url)
@@ -1307,6 +1497,24 @@ function Browser() {
return
}
+ // Clean up camera streams when navigating away from a page
+ if (navState.url !== activeTab.url && activeCameraStreams.current.has(activeTab.id.toString())) {
+ console.log('Cleaning up camera streams for tab navigation')
+ activeCameraStreams.current.delete(activeTab.id.toString())
+
+ // Inject script to stop any active media streams
+ activeTab.webviewRef?.current?.injectJavaScript(`
+ (function() {
+ if (window.__activeMediaStreams) {
+ window.__activeMediaStreams.forEach(stream => {
+ stream.getTracks().forEach(track => track.stop());
+ });
+ window.__activeMediaStreams = [];
+ }
+ })();
+ `)
+ }
+
// Log navigation state changes with back/forward capabilities
console.log('🌐 Navigation State Change:', {
url: navState.url,
@@ -1467,29 +1675,7 @@ function Browser() {
[updateActiveTab]
)
- const BookmarksScene = useMemo(() => {
- return () => (
- {
- // Filter out invalid URLs to prevent favicon errors
- return (
- bookmark.url &&
- bookmark.url !== kNEW_TAB_URL &&
- isValidUrl(bookmark.url) &&
- !bookmark.url.includes('about:blank')
- )
- })
- .reverse()}
- setStartingUrl={handleSetStartingUrl}
- onRemoveBookmark={removeBookmark}
- onRemoveDefaultApp={removeDefaultApp}
- removedDefaultApps={removedDefaultApps}
- hideHeader={true}
- showOnlyBookmarks={true}
- />
- )
- }, [bookmarkStore.bookmarks, handleSetStartingUrl, removeBookmark, removeDefaultApp, removedDefaultApps])
+ // BookmarksScene will be defined after toggleInfoDrawer
const HistoryScene = React.useCallback(() => {
return (
@@ -1559,10 +1745,40 @@ function Browser() {
/* INFO DRAWER NAV */
/* -------------------------------------------------------------------------- */
const toggleInfoDrawer = useCallback((open: boolean, route: typeof infoDrawerRoute = 'root') => {
+ console.log('toggleInfoDrawer called with:', { open, route, isFullscreen, showInfoDrawer })
setInfoDrawerRoute(route)
setShowInfoDrawer(open)
+ console.log('After setShowInfoDrawer, new value should be:', open)
}, [])
+ const BookmarksScene = useMemo(() => {
+ return () => (
+ {
+ // Filter out invalid URLs to prevent favicon errors
+ return (
+ bookmark.url &&
+ bookmark.url !== kNEW_TAB_URL &&
+ isValidUrl(bookmark.url) &&
+ !bookmark.url.includes('about:blank')
+ )
+ })
+ .reverse()}
+ setStartingUrl={handleSetStartingUrl}
+ onRemoveBookmark={removeBookmark}
+ onRemoveDefaultApp={removeDefaultApp}
+ removedDefaultApps={removedDefaultApps}
+ onCloseModal={() => {
+ // Just close the drawer - the bookmark is already added by the component
+ toggleInfoDrawer(false)
+ }}
+ hideHeader={true}
+ showOnlyBookmarks={true}
+ />
+ )
+ }, [bookmarkStore.bookmarks, handleSetStartingUrl, removeBookmark, removeDefaultApp, removedDefaultApps, toggleInfoDrawer])
+
useEffect(() => {
Animated.timing(drawerAnim, {
toValue: showInfoDrawer ? 1 : 0,
@@ -1788,9 +2004,12 @@ function Browser() {
injectedJavaScript={injectedJavaScript}
onNavigationStateChange={handleNavStateChange}
userAgent={isDesktopView ? desktopUserAgent : mobileUserAgent}
+ mediaPlaybackRequiresUserAction={false}
+ allowsInlineMediaPlayback={true}
+ // Deny all WebView permissions to prevent native camera access
+ onPermissionRequest={() => false}
onError={(syntheticEvent: any) => {
const { nativeEvent } = syntheticEvent
- // Ignore favicon errors for about:blank
if (nativeEvent.url?.includes('favicon.ico') && activeTab?.url === kNEW_TAB_URL) {
return
}
@@ -1833,16 +2052,16 @@ function Browser() {
>
{/* deggen: Back Button unless address bar is active, in which case it's the share button */}
{addressFocused ? null
- :
-
- }
-
+ :
+
+ }
+
{activeTab?.canGoForward &&
)}
-
{Platform.OS !== 'ios' && (
{
+ const handleGetStarted = useCallback(async () => {
try {
await analytics().logEvent('get_started_tapped', {
screen: 'onboarding'
@@ -53,7 +53,6 @@ export default function LoginScreen() {
throw new Error(`Failed to fetch info: ${res.status}`)
}
const wabInfo = await res.json()
- console.log({ wabInfo, selectedWabUrl, selectedMethod, selectedNetwork, selectedStorageUrl })
const finalConfig = {
wabUrl: selectedWabUrl,
wabInfo,
@@ -85,7 +84,7 @@ export default function LoginScreen() {
} finally {
setLoading(false)
}
- }
+ }, [selectedWabUrl, selectedMethod, selectedNetwork, selectedStorageUrl, finalizeConfig, setItem, getSnap, managers?.walletManager])
// Config modal state
const [showConfig, setShowConfig] = useState(false)
@@ -102,11 +101,8 @@ export default function LoginScreen() {
const handleConfigured = async () => {
// After successful config, proceed with auth
try {
- const finalConfig = JSON.parse((await getItem('finalConfig')) || '')
- const success = finalizeConfig(finalConfig)
- if (!success) {
- return
- }
+ // The ConfigModal has already called finalizeConfig() with the new configuration
+ // No need to load from storage - the wallet context already has the updated values
const snap = await getSnap()
if (!snap) {
router.push('/auth/phone')
@@ -175,7 +171,17 @@ export default function LoginScreen() {
marginTop: 12
}
]}
- onPress={() => {
+ onPress={handleConfig}
+ >
+
+
+ {t('configure_providers')}
+
+
+
+ {t('terms_privacy_agreement')}
+
+ {
// Set mode to web2 immediately when button is pressed
setWeb2Mode(true)
@@ -189,22 +195,18 @@ export default function LoginScreen() {
handleGetStarted()
}
)
- }}
- >
- Continue without login
-
-
- {t('terms_privacy_agreement')}
-
-
-
-
- {t('configure_providers')}
-
+ }}>
+ {t('continue_without_login')}
>
)}
+
+
)
}
@@ -226,7 +228,7 @@ const styles = StyleSheet.create({
width: 100,
height: 100,
borderRadius: 50,
- backgroundColor: '#0066cc',
+ backgroundColor: '#487dbf',
justifyContent: 'center',
alignItems: 'center'
},
@@ -257,12 +259,21 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: 'bold'
},
+ continueButtonText: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: '#0066cc'
+ },
configButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 50,
- padding: 10
+ padding: 10,
+ borderWidth: 1,
+ borderColor: '#487dbf',
+ borderRadius: 10,
+ width: '60%'
},
configIconContainer: {
flexDirection: 'row',
@@ -270,7 +281,6 @@ const styles = StyleSheet.create({
marginLeft: 8
},
configButtonText: {
- color: '#0066cc',
fontSize: 14,
marginLeft: 2
},
diff --git a/app/settings.tsx b/app/settings.tsx
index 244b5f7..4acb611 100644
--- a/app/settings.tsx
+++ b/app/settings.tsx
@@ -10,7 +10,7 @@ export default function SettingsScreen() {
const { t } = useTranslation()
const { colors, mode, setThemeMode } = useTheme()
const styles = useThemeStyles()
- const { updateSettings, settings, logout } = useWallet()
+ const { updateSettings, settings, logout, selectedWabUrl, selectedStorageUrl, selectedNetwork } = useWallet()
// Handle theme mode change
const handleThemeChange = async (newMode: ThemeMode) => {
@@ -30,9 +30,95 @@ export default function SettingsScreen() {
return (
-
-
- {t('settings')}
+
+ {/* Account Section */}
+
+ {t('wallet_configuration')}
+
+ {/* Wallet Configuration URLs */}
+
+ {/* WAB URL */}
+
+
+ {t('wab_url')}
+
+
+
+ {selectedWabUrl || 'Not configured'}
+
+
+
+
+ {/* Wallet Storage URL */}
+
+
+ {t('wallet_storage_url')}
+
+
+
+ {selectedStorageUrl || 'Not configured'}
+
+
+
+
+ {/* NETWORK */}
+
+
+ {t('bsv_network')}
+
+
+
+ {selectedNetwork || 'Not configured'}
+
+
+
+
+ {/* Explanation text */}
+
+ {t('logout_to_change_urls')}
+
+
+
+
+
+
+ {t('logout')}
+
+
+
{/* Theme Section */}
@@ -103,23 +189,7 @@ export default function SettingsScreen() {
- {/* Account Section */}
-
- {t('account')}
-
-
-
-
- {t('logout')}
-
-
-
-
{/* Other Settings Sections can be added here */}
-
)
diff --git a/components/RecommendedApps.tsx b/components/RecommendedApps.tsx
index a668165..9f9e64a 100644
--- a/components/RecommendedApps.tsx
+++ b/components/RecommendedApps.tsx
@@ -1,4 +1,5 @@
import React, { useState, useMemo, useCallback } from 'react'
+import { observer } from 'mobx-react-lite'
import {
View,
Text,
@@ -17,6 +18,8 @@ import { useTheme } from '@/context/theme/ThemeContext'
import { useWallet } from '@/context/WalletContext'
import { useBrowserMode } from '@/context/BrowserModeContext'
import { useTranslation } from 'react-i18next'
+import bookmarkStore from '@/stores/BookmarkStore'
+import tabStore from '@/stores/TabStore'
interface App {
domain: string
@@ -33,6 +36,7 @@ interface RecommendedAppsProps {
onRemoveBookmark?: (url: string) => void
onRemoveDefaultApp?: (url: string) => void
removedDefaultApps?: string[]
+ onCloseModal?: () => void // Handler to close the modal
// Homepage customization props
homepageSettings?: {
showBookmarks: boolean
@@ -74,7 +78,7 @@ const defaultApps: App[] = [
/* RECOMMENDED APPS COMPONENT */
/* -------------------------------------------------------------------------- */
-export const RecommendedApps = ({
+export const RecommendedApps = observer(({
setStartingUrl,
includeBookmarks = [],
hideHeader = false,
@@ -83,6 +87,7 @@ export const RecommendedApps = ({
onRemoveBookmark,
onRemoveDefaultApp,
removedDefaultApps = [],
+ onCloseModal,
homepageSettings,
onUpdateHomepageSettings
}: RecommendedAppsProps) => {
@@ -92,7 +97,7 @@ export const RecommendedApps = ({
const { t } = useTranslation()
const [searchQuery, setSearchQuery] = useState('')
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
- const [isDesktopView, setIsDesktopView] = useState(false)
+ const [bookmarkRefresh, setBookmarkRefresh] = useState(0)
// Context menu state
const [contextMenuVisible, setContextMenuVisible] = useState(false)
@@ -140,6 +145,21 @@ export const RecommendedApps = ({
setSelectedApp(null)
}, [])
+ const handleAddBookmark = useCallback(() => {
+ const activeTab = tabStore.activeTab
+ if (activeTab && activeTab.url && activeTab.url !== 'about:blank' && !activeTab.url.includes('metanet://')) {
+ const title = activeTab.title || activeTab.url
+ bookmarkStore.addBookmark(title, activeTab.url)
+ // Close the modal after adding bookmark
+ if (onCloseModal) {
+ // Use setTimeout to ensure the bookmark is added first, then close
+ setTimeout(() => {
+ onCloseModal()
+ }, 100)
+ }
+ }
+ }, [onCloseModal])
+
/* -------------------------- prepare separate data sources -------------------------- */
const filteredDefaultApps = useMemo(() => {
if (showOnlyBookmarks) return []
@@ -160,7 +180,11 @@ export const RecommendedApps = ({
}, [recentApps, showOnlyBookmarks, homepageSettings, isWeb2Mode])
const processedBookmarks = useMemo(() => {
- const bookmarks = includeBookmarks.map(bm => ({
+ // Always use bookmarks directly from the store to ensure reactivity
+ // This ensures the component updates when bookmarks are added/removed
+ const storeBookmarks = bookmarkStore.bookmarks || []
+ console.log('Processing bookmarks, count:', storeBookmarks.length)
+ const bookmarks = storeBookmarks.map(bm => ({
domain: bm.url,
appName: bm.title || bm.url,
appIconImageUrl: `${bm.url.replace(/\/$/, '')}/favicon.ico`
@@ -172,7 +196,7 @@ export const RecommendedApps = ({
}
return bookmarks
- }, [includeBookmarks, showOnlyBookmarks, limitBookmarks])
+ }, [bookmarkStore.bookmarks, showOnlyBookmarks, limitBookmarks, bookmarkRefresh])
// Combined for search functionality
const allApps = useMemo(() => {
@@ -242,6 +266,9 @@ export const RecommendedApps = ({
return (
+
+ {/* Removed Add Bookmark button from top - now at bottom */}
+
{showOnlyBookmarks && (
+ {/* Add Bookmark Button - Only show in bookmark modal */}
+ {showOnlyBookmarks && (
+
+
+
+ {t('add_bookmark')}
+
+
+ )}
+
{/* Customize Homepage Modal */}
{showCustomizeModal && homepageSettings && onUpdateHomepageSettings && (
)
-}
+})
/* -------------------------------------------------------------------------- */
/* CSS */
@@ -683,5 +724,23 @@ const componentStyles = StyleSheet.create({
backgroundColor: 'rgba(0,0,0,1)',
justifyContent: 'center',
alignItems: 'center'
+ },
+ addBookmarkSection: {
+ padding: 16,
+ paddingBottom: 36,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderTopColor: 'rgba(0,0,0,0.1)'
+ },
+ addBookmarkButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 16,
+ borderRadius: 8,
+ gap: 8
+ },
+ addBookmarkText: {
+ fontSize: 16,
+ fontWeight: '600'
}
})
diff --git a/components/RecoveryKeySaver.tsx b/components/RecoveryKeySaver.tsx
index 135a8a6..5fbca6f 100644
--- a/components/RecoveryKeySaver.tsx
+++ b/components/RecoveryKeySaver.tsx
@@ -127,13 +127,11 @@ const RecoveryKeySaver = () => {
- Secure Access Backup and Recovery
+ Save Your Recovery Key Now
{!affirmative1 && (
- Save Your Recovery Key Now:
-
{
color={colors.buttonText}
style={styles.buttonIcon}
/>
- Copy to Clipboard
+ Copy
{
setAffirmative1(!affirmative1)}
- label="I have saved my recovery key in a secure location"
+ label="I have saved my recovery key"
/>
{affirmative1 && (
- Any 2 of 3 factors are required to access your data:
-
-
-
- Phone, Password, Recovery Key
-
-
-
- When you lose your phone or forget your password, you must use the other factors to re-establish
- secure control. This is a perfectly normal and unavoidable fact of life. However -
+ Any 2 of 3 factors are required to access your data: Phone, Password, Recovery Key
- Loss of more than one factor will result in TOTAL LOSS of access to all assets, encrypted data,
- and certificates.
+ Loss of two factors = PERMANENT TOTAL LOSS of all assets, and data.
@@ -231,15 +219,15 @@ const RecoveryKeySaver = () => {
style={[
styles.buttonPrimary,
{
- backgroundColor: isAllChecked ? '#006600' : colors.inputBorder,
- opacity: isAllChecked ? 1 : 0.5
+ backgroundColor: colors.buttonBackground,
+ opacity: isAllChecked ? 1 : 0.2
}
]}
onPress={onKeySaved}
disabled={!isAllChecked}
>
Securely Saved
-
+
@@ -257,7 +245,6 @@ const styles = StyleSheet.create({
alignItems: 'center'
},
keyboardAvoid: {
- width: '90%',
maxWidth: 500
},
modalContent: {
@@ -271,7 +258,6 @@ const styles = StyleSheet.create({
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
- maxHeight: '80%'
},
scrollView: {
maxHeight: 450,
@@ -319,7 +305,7 @@ const styles = StyleSheet.create({
flex: 0.48
},
buttonIcon: {
- marginRight: 8
+ marginLeft: 8
},
warningBox: {
borderWidth: 1,
@@ -379,7 +365,7 @@ const styles = StyleSheet.create({
},
buttonText: {
fontSize: 16,
- fontWeight: '500'
+ fontWeight: '500',
},
buttonSecondaryText: {
fontSize: 16
diff --git a/components/Web3BenefitsModal.tsx b/components/Web3BenefitsModal.tsx
index f668e78..ac718b3 100644
--- a/components/Web3BenefitsModal.tsx
+++ b/components/Web3BenefitsModal.tsx
@@ -63,36 +63,35 @@ const Web3BenefitsModal: React.FC = ({
{/* Content - Simple approach */}
- The benefits of web3 are as follows:
+ A Web3 identity brings you:
+ Micropayments
- • Never login again - One identity for every Web3 app. No more passwords or sign-ups.
+ where creators to earn directly.
+ Private Identity
- • Instant everything - Payments, access, verification - all happen in seconds.
+ with mutual auth means no signups or logins.
+ Data Sovereignty
- • You own your data - No companies tracking you or selling your information.
-
-
- • Works everywhere - Access thousands of Web3 apps with the same identity.
-
-
- • Future-proof - Be early to the next generation of the internet.
+ you're in control, with no 3rd party tracking.
+ Become an early adopter and lead your peers to the future of everything.
+
{/* Action Buttons */}
- 🚀 Get My Web3 Identity (30s)
+ Get Started
- Maybe later
+ Maybe Later
@@ -165,10 +164,13 @@ const styles = StyleSheet.create({
paddingHorizontal: 12,
alignItems: 'center',
alignSelf: 'center',
- marginTop: 8
+ marginTop: 8,
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: '#666'
},
secondaryButtonText: {
- fontSize: 11,
+ fontSize: 14,
fontWeight: '300',
opacity: 0.5
}
diff --git a/context/WalletContext.tsx b/context/WalletContext.tsx
index f8a0e67..7903958 100644
--- a/context/WalletContext.tsx
+++ b/context/WalletContext.tsx
@@ -742,7 +742,7 @@ export const WalletContextProvider: React.FC = ({ children =
passwordRetriever,
recoveryKeySaver,
configStatus,
- managers.walletManager,
+ walletBuilt,
selectedNetwork,
selectedWabUrl,
buildWallet,
diff --git a/ios/Metanet/Info.plist b/ios/Metanet/Info.plist
index 30affe0..39ad9fd 100644
--- a/ios/Metanet/Info.plist
+++ b/ios/Metanet/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 0.3.0
+ 0.3.2
CFBundleSignature
????
CFBundleURLTypes
diff --git a/package-lock.json b/package-lock.json
index a17c663..27af7bb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,17 @@
{
"name": "metanet-mobile",
- "version": "0.3.0",
+ "version": "0.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metanet-mobile",
- "version": "0.3.0",
+ "version": "0.3.2",
"license": "Open BSV",
"dependencies": {
- "@bsv/message-box-client": "^1.2.3",
- "@bsv/sdk": "^1.6.20",
- "@bsv/wallet-toolbox-mobile": "^1.5.20",
+ "@bsv/message-box-client": "^1.2.6",
+ "@bsv/sdk": "^1.6.24",
+ "@bsv/wallet-toolbox-mobile": "^1.6.4",
"@dicebear/core": "^9.2.2",
"@dicebear/identicon": "^9.2.2",
"@expo/vector-icons": "^14.1.0",
@@ -1595,9 +1595,9 @@
}
},
"node_modules/@bsv/message-box-client": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@bsv/message-box-client/-/message-box-client-1.2.4.tgz",
- "integrity": "sha512-nU7bxo4JHNCZfO+8qF8BgmQBfouhbKq+JoLZCD0Ljh8XqpkvMeAy1mj5etk8aYDkVEJ+qSyGD01NkPfJ6P6shA==",
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@bsv/message-box-client/-/message-box-client-1.2.6.tgz",
+ "integrity": "sha512-3Ul7IfwtchJLNRFc6YUxMRD3at+6xoM0Yjhp8XUVCSovMWcBOLz0yrzN9DsTN9DXordBWprkjd96zboSo3klbw==",
"license": "SEE LICENSE IN LICENSE.txt",
"dependencies": {
"@bsv/authsocket-client": "^1.0.11",
@@ -1605,18 +1605,18 @@
}
},
"node_modules/@bsv/sdk": {
- "version": "1.6.23",
- "resolved": "https://registry.npmjs.org/@bsv/sdk/-/sdk-1.6.23.tgz",
- "integrity": "sha512-l2ElERB49x1yud1kC3kP5E8s1SFw379bLJE+TRXLyaP2Gbje/5FFlvkjg7e78Xni3fYSUHfHHIsxmWhbE/Ls4A==",
+ "version": "1.6.24",
+ "resolved": "https://registry.npmjs.org/@bsv/sdk/-/sdk-1.6.24.tgz",
+ "integrity": "sha512-st8LfNZGyj5EHPedruTfttmMQjw9NeX/TGlX+hq+TG3rAzpw17AxY8SlseDzIa5ZrEcgIxuhtEaeKD+kbphqDA==",
"license": "SEE LICENSE IN LICENSE.txt"
},
"node_modules/@bsv/wallet-toolbox-mobile": {
- "version": "1.5.21",
- "resolved": "https://registry.npmjs.org/@bsv/wallet-toolbox-mobile/-/wallet-toolbox-mobile-1.5.21.tgz",
- "integrity": "sha512-P7tqwNrrB1ioFPCyBwG/j025ABKdFWfHnQ3iR7IueYXZMon6K2pxveUNMkLgWFCCsSXY7uGZwb+gtudAwBT29w==",
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/@bsv/wallet-toolbox-mobile/-/wallet-toolbox-mobile-1.6.4.tgz",
+ "integrity": "sha512-OXHkWJ7YElZ1klRXDQWIyCw3njFaplXtxsNhjxaKU1dHfuNJsaBD9kJOHXJ80uBh9exWeFFLNquozM7Sg7D7Rg==",
"license": "SEE LICENSE IN license.md",
"dependencies": {
- "@bsv/sdk": "^1.6.23"
+ "@bsv/sdk": "^1.6.24"
}
},
"node_modules/@callstack/react-theme-provider": {
diff --git a/package.json b/package.json
index 8f3447a..0243434 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"description": "Enabling identity, micropayments, and use of Metanet websites on mobile devices",
"author": "BSV Association",
"main": "index.js",
- "version": "0.3.0",
+ "version": "0.3.2",
"license": "Open BSV",
"scripts": {
"apk-android": "eas build --profile production --platform android --local --verbose-logs --build-logger-level trace",
@@ -25,9 +25,9 @@
"clean": "rm -rf yarn.lock && rm -rf .expo && rm -rf .expo-shared && rm -rf .vscode && rm -rf .idea && rm -rf ios/Pods && rm -rf android/.gradle && rm -rf android/app/build && rm -rf ./ios"
},
"dependencies": {
- "@bsv/message-box-client": "^1.2.3",
- "@bsv/sdk": "^1.6.20",
- "@bsv/wallet-toolbox-mobile": "^1.5.20",
+ "@bsv/message-box-client": "^1.2.6",
+ "@bsv/sdk": "^1.6.24",
+ "@bsv/wallet-toolbox-mobile": "^1.6.4",
"@dicebear/core": "^9.2.2",
"@dicebear/identicon": "^9.2.2",
"@expo/vector-icons": "^14.1.0",
diff --git a/plugins/with-fcm-manifest-fixes.js b/plugins/with-fcm-manifest-fixes.js
index 34d0b9a..c2a6227 100644
--- a/plugins/with-fcm-manifest-fixes.js
+++ b/plugins/with-fcm-manifest-fixes.js
@@ -2,7 +2,8 @@ const { withAndroidManifest, AndroidConfig } = require('@expo/config-plugins');
const withFcmManifestFixes = (config) => {
return withAndroidManifest(config, (cfg) => {
- const { manifest } = cfg.modResults;
+ const androidManifest = cfg.modResults;
+ const { manifest } = androidManifest;
// Ensure tools namespace on
manifest.$ = manifest.$ || {};
@@ -11,7 +12,7 @@ const withFcmManifestFixes = (config) => {
}
// Get
- const app = AndroidConfig.Manifest.getMainApplication(manifest);
+ const app = AndroidConfig.Manifest.getMainApplicationOrThrow(androidManifest);
if (!app) { throw new Error('Main application not found in AndroidManifest'); }
// Helper to upsert by android:name
diff --git a/utils/translations.tsx b/utils/translations.tsx
index 09d7aca..42b59ec 100644
--- a/utils/translations.tsx
+++ b/utils/translations.tsx
@@ -185,6 +185,9 @@ const resources = {
system_default: 'System Default',
account: 'Account',
logout: 'Logout',
+ wallet_configuration: 'Wallet Configuration',
+ wallet_storage_url: 'Wallet Storage URL',
+ logout_to_change_urls: 'You need to logout to change these URLs.',
// Security
security: 'Security',
@@ -411,6 +414,9 @@ const resources = {
system_default: '系统默认',
account: '账户',
logout: '退出登录',
+ wallet_configuration: '钱包配置',
+ wallet_storage_url: '钱包存储URL',
+ logout_to_change_urls: '您需要退出登录以更改这些URL。',
// Security
security: '安全',
@@ -637,6 +643,9 @@ const resources = {
system_default: 'सिस्टम डिफ़ॉल्ट',
account: 'खाता',
logout: 'लॉगआउट',
+ wallet_configuration: 'वॉलेट कॉन्फ़िगरेशन',
+ wallet_storage_url: 'वॉलेट स्टोरेज URL',
+ logout_to_change_urls: 'इन URLs को बदलने के लिए आपको लॉगआउट करना होगा।',
// Security
security: 'सुरक्षा',