feat(web): Add mobile bridge support for P2P WebView integration

- Add mobile-bridge.ts utility for native app communication
- Add useMobileBridge.ts React hook
- Update AuthContext to detect and use native wallet
- Update PezkuwiContext to connect mobile wallet automatically
- Update WalletContext to sign transactions via native bridge

Mobile app can now seamlessly use web P2P features with native wallet.
This commit is contained in:
2026-01-15 10:00:11 +03:00
parent ba74fe4298
commit 298a2c57f1
5 changed files with 415 additions and 11 deletions
+38 -2
View File
@@ -1,6 +1,7 @@
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { supabase } from '@/lib/supabase';
import { User } from '@supabase/supabase-js';
import { isMobileApp, getNativeWalletAddress, getNativeAccountName } from '@/lib/mobile-bridge';
// Session timeout configuration
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
@@ -159,7 +160,31 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
}, []);
// Setup native mobile wallet if running in mobile app
const setupMobileWallet = useCallback(() => {
if (isMobileApp()) {
const nativeAddress = getNativeWalletAddress();
const nativeAccountName = getNativeAccountName();
if (nativeAddress) {
// Store native wallet address for admin checks and wallet operations
localStorage.setItem('selectedWallet', nativeAddress);
if (nativeAccountName) {
localStorage.setItem('selectedWalletName', nativeAccountName);
}
if (import.meta.env.DEV) {
console.log('[Mobile] Native wallet detected:', nativeAddress);
}
// Dispatch wallet change event
window.dispatchEvent(new Event('walletChanged'));
}
}
}, []);
useEffect(() => {
// Setup mobile wallet first
setupMobileWallet();
// Check active sessions and sets the user
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
@@ -178,17 +203,28 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setLoading(false);
});
// Listen for wallet changes (from PezkuwiContext)
// Listen for wallet changes (from PezkuwiContext or native bridge)
const handleWalletChange = () => {
checkAdminStatus();
};
window.addEventListener('walletChanged', handleWalletChange);
// Listen for native bridge ready event (mobile app)
const handleNativeReady = () => {
if (import.meta.env.DEV) {
console.log('[Mobile] Native bridge ready');
}
setupMobileWallet();
checkAdminStatus();
};
window.addEventListener('pezkuwi-native-ready', handleNativeReady);
return () => {
subscription.unsubscribe();
window.removeEventListener('walletChanged', handleWalletChange);
window.removeEventListener('pezkuwi-native-ready', handleNativeReady);
};
}, [checkAdminStatus]);
}, [checkAdminStatus, setupMobileWallet]);
const signIn = async (email: string, password: string, rememberMe: boolean = false) => {
try {
+80 -8
View File
@@ -3,6 +3,7 @@ import { ApiPromise, WsProvider } from '@pezkuwi/api';
import { web3Accounts, web3Enable } from '@pezkuwi/extension-dapp';
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
import { DEFAULT_ENDPOINT } from '../../../shared/blockchain/pezkuwi';
import { isMobileApp, getNativeWalletAddress, getNativeAccountName } from '@/lib/mobile-bridge';
interface PezkuwiContextType {
api: ApiPromise | null;
@@ -127,9 +128,36 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [endpoint]);
// Auto-restore wallet on page load
// Auto-restore wallet on page load (or setup mobile wallet)
useEffect(() => {
const restoreWallet = async () => {
// Check if running in mobile app
if (isMobileApp()) {
const nativeAddress = getNativeWalletAddress();
const nativeAccountName = getNativeAccountName();
if (nativeAddress) {
// Create a virtual account for the mobile wallet
const mobileAccount: InjectedAccountWithMeta = {
address: nativeAddress,
meta: {
name: nativeAccountName || 'Mobile Wallet',
source: 'pezkuwi-mobile',
},
type: 'sr25519',
};
setAccounts([mobileAccount]);
handleSetSelectedAccount(mobileAccount);
if (import.meta.env.DEV) {
console.log('[Mobile] Native wallet connected:', nativeAddress.slice(0, 8) + '...');
}
return;
}
}
// Desktop: Try to restore from localStorage
const savedAddress = localStorage.getItem('selectedWallet');
if (!savedAddress) return;
@@ -148,25 +176,69 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
setAccounts(allAccounts);
handleSetSelectedAccount(savedAccount);
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.log('✅ Wallet restored:', savedAddress.slice(0, 8) + '...');
console.log('✅ Wallet restored:', savedAddress.slice(0, 8) + '...');
}
}
} catch (err) {
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.error('Failed to restore wallet:', err);
console.error('Failed to restore wallet:', err);
}
}
};
restoreWallet();
// Listen for native bridge ready event (mobile)
const handleNativeReady = () => {
if (import.meta.env.DEV) {
console.log('[Mobile] Native bridge ready, restoring wallet');
}
restoreWallet();
};
window.addEventListener('pezkuwi-native-ready', handleNativeReady);
return () => {
window.removeEventListener('pezkuwi-native-ready', handleNativeReady);
};
}, []);
// Connect wallet (Pezkuwi.js extension)
// Connect wallet (Pezkuwi.js extension or native mobile)
const connectWallet = async () => {
try {
setError(null);
// Enable extension
// Check if running in mobile app
if (isMobileApp()) {
const nativeAddress = getNativeWalletAddress();
const nativeAccountName = getNativeAccountName();
if (nativeAddress) {
// Create a virtual account for the mobile wallet
const mobileAccount: InjectedAccountWithMeta = {
address: nativeAddress,
meta: {
name: nativeAccountName || 'Mobile Wallet',
source: 'pezkuwi-mobile',
},
type: 'sr25519',
};
setAccounts([mobileAccount]);
handleSetSelectedAccount(mobileAccount);
if (import.meta.env.DEV) {
console.log('[Mobile] Native wallet connected:', nativeAddress.slice(0, 8) + '...');
}
return;
} else {
// Request wallet connection from native app
setError('Please connect your wallet in the app');
return;
}
}
// Desktop: Enable extension
const extensions = await web3Enable('PezkuwiChain');
if (extensions.length === 0) {
@@ -176,7 +248,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
}
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.log('✅ Pezkuwi.js extension enabled');
console.log('✅ Pezkuwi.js extension enabled');
}
// Get accounts
@@ -199,12 +271,12 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
handleSetSelectedAccount(accountToSelect);
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.log(`✅ Found ${allAccounts.length} account(s)`);
console.log(`✅ Found ${allAccounts.length} account(s)`);
}
} catch (err) {
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.error('❌ Wallet connection failed:', err);
console.error('❌ Wallet connection failed:', err);
}
setError('Failed to connect wallet');
}
+19 -1
View File
@@ -10,6 +10,7 @@ import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@pezkuwi/lib/wallet';
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
import type { Signer } from '@pezkuwi/api/types';
import { web3FromAddress } from '@pezkuwi/extension-dapp';
import { isMobileApp, getNativeWalletAddress, signTransactionNative } from '@/lib/mobile-bridge';
interface TokenBalances {
HEZ: string;
@@ -165,11 +166,28 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
}
try {
// Check if running in mobile app - use native bridge for signing
if (isMobileApp()) {
if (import.meta.env.DEV) console.log('[Mobile] Using native bridge for transaction signing');
// Get extrinsic hex for native signing
const extrinsicHex = (tx as { toHex?: () => string }).toHex?.() || '';
// Sign via native bridge
const signature = await signTransactionNative(extrinsicHex);
// Submit the signed transaction
// Note: The native app signs and may also submit, so we return the signature as hash
// In production, the mobile app handles the full submit flow
return signature;
}
// Desktop: Use browser extension for signing
const { web3FromAddress } = await import('@pezkuwi/extension-dapp');
const injector = await web3FromAddress(pezkuwi.selectedAccount.address);
// Sign and send transaction
const hash = await tx.signAndSend(
const hash = await (tx as { signAndSend: (address: string, options: { signer: unknown }) => Promise<{ toHex: () => string }> }).signAndSend(
pezkuwi.selectedAccount.address,
{ signer: injector.signer }
);