mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 13:37:59 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
isMobileApp,
|
||||
getPlatform,
|
||||
getNativeWalletAddress,
|
||||
getNativeAccountName,
|
||||
isNativeWalletConnected,
|
||||
requestNativeWalletConnection,
|
||||
signTransactionNative,
|
||||
navigateBackNative,
|
||||
type MobileBridgeState,
|
||||
} from '@/lib/mobile-bridge';
|
||||
|
||||
/**
|
||||
* React hook for mobile bridge integration
|
||||
*
|
||||
* Provides reactive state and methods for interacting with the native mobile app.
|
||||
* Automatically updates when the native bridge becomes ready.
|
||||
*/
|
||||
export function useMobileBridge() {
|
||||
const [state, setState] = useState<MobileBridgeState>({
|
||||
isMobile: false,
|
||||
platform: 'web',
|
||||
walletAddress: null,
|
||||
accountName: null,
|
||||
isWalletConnected: false,
|
||||
});
|
||||
|
||||
// Update state from native bridge
|
||||
const updateState = useCallback(() => {
|
||||
setState({
|
||||
isMobile: isMobileApp(),
|
||||
platform: getPlatform(),
|
||||
walletAddress: getNativeWalletAddress(),
|
||||
accountName: getNativeAccountName(),
|
||||
isWalletConnected: isNativeWalletConnected(),
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Initial state
|
||||
updateState();
|
||||
|
||||
// Listen for native bridge ready event
|
||||
const handleNativeReady = (event: CustomEvent) => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[MobileBridge] Native bridge ready:', event.detail);
|
||||
}
|
||||
updateState();
|
||||
};
|
||||
|
||||
// Listen for wallet changes from native
|
||||
const handleWalletChange = () => {
|
||||
updateState();
|
||||
};
|
||||
|
||||
window.addEventListener('pezkuwi-native-ready', handleNativeReady as EventListener);
|
||||
window.addEventListener('walletChanged', handleWalletChange);
|
||||
|
||||
// Check periodically in case bridge loads after initial render
|
||||
const checkInterval = setInterval(() => {
|
||||
if (isMobileApp() && !state.isMobile) {
|
||||
updateState();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Clear interval after 5 seconds (bridge should be ready by then)
|
||||
const clearTimer = setTimeout(() => {
|
||||
clearInterval(checkInterval);
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('pezkuwi-native-ready', handleNativeReady as EventListener);
|
||||
window.removeEventListener('walletChanged', handleWalletChange);
|
||||
clearInterval(checkInterval);
|
||||
clearTimeout(clearTimer);
|
||||
};
|
||||
}, [updateState, state.isMobile]);
|
||||
|
||||
// Connect wallet via native app
|
||||
const connectWallet = useCallback(() => {
|
||||
if (state.isMobile) {
|
||||
requestNativeWalletConnection();
|
||||
}
|
||||
}, [state.isMobile]);
|
||||
|
||||
// Sign transaction via native app
|
||||
const signTransaction = useCallback(async (extrinsicHex: string): Promise<string> => {
|
||||
if (!state.isMobile) {
|
||||
throw new Error('Not running in mobile app');
|
||||
}
|
||||
return signTransactionNative(extrinsicHex);
|
||||
}, [state.isMobile]);
|
||||
|
||||
// Navigate back in native app
|
||||
const goBack = useCallback(() => {
|
||||
if (state.isMobile) {
|
||||
navigateBackNative();
|
||||
}
|
||||
}, [state.isMobile]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
connectWallet,
|
||||
signTransaction,
|
||||
goBack,
|
||||
updateState,
|
||||
};
|
||||
}
|
||||
|
||||
export default useMobileBridge;
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Mobile Bridge Utility
|
||||
*
|
||||
* Handles communication between the web app and native mobile app (React Native WebView).
|
||||
* When running inside the Pezkuwi mobile app, this bridge enables:
|
||||
* - Native wallet integration (address, signing)
|
||||
* - Platform detection
|
||||
* - Native navigation
|
||||
*/
|
||||
|
||||
// Type definitions for the native bridge
|
||||
declare global {
|
||||
interface Window {
|
||||
PEZKUWI_MOBILE?: boolean;
|
||||
PEZKUWI_PLATFORM?: 'ios' | 'android';
|
||||
PEZKUWI_ADDRESS?: string;
|
||||
PEZKUWI_ACCOUNT_NAME?: string;
|
||||
ReactNativeWebView?: {
|
||||
postMessage: (message: string) => void;
|
||||
};
|
||||
PezkuwiNativeBridge?: {
|
||||
signTransaction: (extrinsicHex: string, callback: (signature: string | null, error: string | null) => void) => void;
|
||||
connectWallet: () => void;
|
||||
goBack: () => void;
|
||||
isWalletConnected: () => boolean;
|
||||
getAddress: () => string | null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface MobileBridgeState {
|
||||
isMobile: boolean;
|
||||
platform: 'ios' | 'android' | 'web';
|
||||
walletAddress: string | null;
|
||||
accountName: string | null;
|
||||
isWalletConnected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running inside mobile WebView
|
||||
*/
|
||||
export function isMobileApp(): boolean {
|
||||
return typeof window !== 'undefined' && window.PEZKUWI_MOBILE === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current platform
|
||||
*/
|
||||
export function getPlatform(): 'ios' | 'android' | 'web' {
|
||||
if (!isMobileApp()) return 'web';
|
||||
return window.PEZKUWI_PLATFORM || 'android';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get native wallet address (if connected in mobile app)
|
||||
*/
|
||||
export function getNativeWalletAddress(): string | null {
|
||||
if (!isMobileApp()) return null;
|
||||
return window.PEZKUWI_ADDRESS || window.PezkuwiNativeBridge?.getAddress() || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get native account name
|
||||
*/
|
||||
export function getNativeAccountName(): string | null {
|
||||
if (!isMobileApp()) return null;
|
||||
return window.PEZKUWI_ACCOUNT_NAME || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if native wallet is connected
|
||||
*/
|
||||
export function isNativeWalletConnected(): boolean {
|
||||
if (!isMobileApp()) return false;
|
||||
return window.PezkuwiNativeBridge?.isWalletConnected() || !!window.PEZKUWI_ADDRESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request wallet connection from native app
|
||||
*/
|
||||
export function requestNativeWalletConnection(): void {
|
||||
if (!isMobileApp()) return;
|
||||
window.PezkuwiNativeBridge?.connectWallet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign transaction using native wallet
|
||||
* Returns a promise that resolves with the signature or rejects with error
|
||||
*/
|
||||
export function signTransactionNative(extrinsicHex: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!isMobileApp() || !window.PezkuwiNativeBridge) {
|
||||
reject(new Error('Native bridge not available'));
|
||||
return;
|
||||
}
|
||||
|
||||
window.PezkuwiNativeBridge.signTransaction(extrinsicHex, (signature, error) => {
|
||||
if (error) {
|
||||
reject(new Error(error));
|
||||
} else if (signature) {
|
||||
resolve(signature);
|
||||
} else {
|
||||
reject(new Error('No signature returned'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate back in native app
|
||||
*/
|
||||
export function navigateBackNative(): void {
|
||||
if (!isMobileApp()) return;
|
||||
window.PezkuwiNativeBridge?.goBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to native app
|
||||
*/
|
||||
export function sendMessageToNative(type: string, payload?: unknown): void {
|
||||
if (!isMobileApp() || !window.ReactNativeWebView) return;
|
||||
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({ type, payload }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current mobile bridge state
|
||||
*/
|
||||
export function getMobileBridgeState(): MobileBridgeState {
|
||||
const isMobile = isMobileApp();
|
||||
return {
|
||||
isMobile,
|
||||
platform: getPlatform(),
|
||||
walletAddress: getNativeWalletAddress(),
|
||||
accountName: getNativeAccountName(),
|
||||
isWalletConnected: isNativeWalletConnected(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to native console (for debugging)
|
||||
*/
|
||||
export function logToNative(message: string, data?: unknown): void {
|
||||
if (!isMobileApp()) {
|
||||
console.log(message, data);
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessageToNative('CONSOLE_LOG', { message, data });
|
||||
}
|
||||
|
||||
// Export a singleton for easy access
|
||||
export const mobileBridge = {
|
||||
isMobileApp,
|
||||
getPlatform,
|
||||
getNativeWalletAddress,
|
||||
getNativeAccountName,
|
||||
isNativeWalletConnected,
|
||||
requestNativeWalletConnection,
|
||||
signTransactionNative,
|
||||
navigateBackNative,
|
||||
sendMessageToNative,
|
||||
getMobileBridgeState,
|
||||
logToNative,
|
||||
};
|
||||
|
||||
export default mobileBridge;
|
||||
Reference in New Issue
Block a user