2 Commits

Author SHA1 Message Date
pezkuwichain 69789548e7 fix: prevent 'API not ready' on mobile by blocking wallet connect until blockchain initializes
- Add isApiInitializing state (true during WS connect, false on ready/fail)
- Add isApiReadyRef for closure-safe polling in connectWalletConnect
- connectWalletConnect now waits up to 30s for API instead of throwing immediately
- WalletModal connect buttons disabled + show spinner while blockchain is initializing
2026-04-27 15:00:58 +03:00
pezkuwichain 86ff43e206 feat: write p2p_user_id to tg_users on Telegram wallet link
TelegramConnect: query tg_users instead of users, resolve visa UUID
from p2p_visa table and store as p2p_user_id for cross-platform P2P.

P2PIdentityContext: when citizen resolves their UUID, backfill
tg_users.p2p_user_id if their wallet is linked to a Telegram account.
2026-04-27 13:31:22 +03:00
4 changed files with 90 additions and 12 deletions
+26 -5
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Wallet, Chrome, ExternalLink, Copy, Check, LogOut, Award, Users, TrendingUp, Shield, Smartphone } from 'lucide-react'; import { Wallet, Chrome, ExternalLink, Copy, Check, LogOut, Award, Users, TrendingUp, Shield, Smartphone, Loader2 } from 'lucide-react';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
import { import {
Dialog, Dialog,
@@ -29,6 +29,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
disconnectWallet, disconnectWallet,
api, api,
isApiReady, isApiReady,
isApiInitializing,
peopleApi, peopleApi,
walletSource, walletSource,
wcPeerName, wcPeerName,
@@ -350,9 +351,19 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
onClick={handleConnect} onClick={handleConnect}
className="w-full bg-gradient-to-r from-purple-600 to-cyan-400 hover:from-purple-700 hover:to-cyan-500" className="w-full bg-gradient-to-r from-purple-600 to-cyan-400 hover:from-purple-700 hover:to-cyan-500"
size="sm" size="sm"
disabled={isApiInitializing}
> >
<Wallet className="mr-2 h-4 w-4" /> {isApiInitializing ? (
{t('walletModal.extensionConnect')} <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('walletModal.connectingBlockchain', 'Connecting to blockchain...')}
</>
) : (
<>
<Wallet className="mr-2 h-4 w-4" />
{t('walletModal.extensionConnect')}
</>
)}
</Button> </Button>
<a <a
href="https://chromewebstore.google.com/search/pezkuwi%7B.js%7D%20extension?hl=en-GB&utm_source=ext_sidebar" href="https://chromewebstore.google.com/search/pezkuwi%7B.js%7D%20extension?hl=en-GB&utm_source=ext_sidebar"
@@ -380,9 +391,19 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
variant="outline" variant="outline"
className="w-full border-purple-500/30 hover:border-purple-500/60 hover:bg-purple-500/10" className="w-full border-purple-500/30 hover:border-purple-500/60 hover:bg-purple-500/10"
size="sm" size="sm"
disabled={isApiInitializing}
> >
<Smartphone className="mr-2 h-4 w-4" /> {isApiInitializing ? (
{t('walletModal.mobileConnect')} <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('walletModal.connectingBlockchain', 'Connecting to blockchain...')}
</>
) : (
<>
<Smartphone className="mr-2 h-4 w-4" />
{t('walletModal.mobileConnect')}
</>
)}
</Button> </Button>
<div className="flex items-center justify-center gap-1 text-xs text-gray-400"> <div className="flex items-center justify-center gap-1 text-xs text-gray-400">
{t('walletModal.mobileComingSoon')} {t('walletModal.mobileComingSoon')}
+15
View File
@@ -54,6 +54,21 @@ export function P2PIdentityProvider({ children }: { children: ReactNode }) {
const uuid = await identityToUUID(fullCitizenNumber); const uuid = await identityToUUID(fullCitizenNumber);
setUserId(uuid); setUserId(uuid);
setVisaNumber(null); setVisaNumber(null);
// If this wallet is linked to a Telegram account and p2p_user_id not set yet,
// backfill it so mini app users see the same P2P identity
if (walletAddress) {
supabase
.from('tg_users')
.select('id, p2p_user_id')
.eq('wallet_address', walletAddress)
.maybeSingle()
.then(({ data: tgUser }) => {
if (tgUser && !tgUser.p2p_user_id) {
supabase.from('tg_users').update({ p2p_user_id: uuid }).eq('id', tgUser.id);
}
});
}
} else if (walletAddress) { } else if (walletAddress) {
// Non-citizen: check for existing visa // Non-citizen: check for existing visa
const { data: visa } = await supabase const { data: visa } = await supabase
+22 -2
View File
@@ -19,7 +19,7 @@ if (typeof window !== 'undefined' && !import.meta.env.DEV) {
}; };
} }
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import React, { createContext, useContext, useEffect, useState, useRef, ReactNode } from 'react';
import { ApiPromise, WsProvider } from '@pezkuwi/api'; import { ApiPromise, WsProvider } from '@pezkuwi/api';
import { web3Accounts, web3Enable } from '@pezkuwi/extension-dapp'; import { web3Accounts, web3Enable } from '@pezkuwi/extension-dapp';
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types'; import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
@@ -49,6 +49,7 @@ interface PezkuwiContextType {
assetHubApi: ApiPromise | null; assetHubApi: ApiPromise | null;
peopleApi: ApiPromise | null; peopleApi: ApiPromise | null;
isApiReady: boolean; isApiReady: boolean;
isApiInitializing: boolean;
isAssetHubReady: boolean; isAssetHubReady: boolean;
isPeopleReady: boolean; isPeopleReady: boolean;
isConnected: boolean; isConnected: boolean;
@@ -79,6 +80,8 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
const [assetHubApi, setAssetHubApi] = useState<ApiPromise | null>(null); const [assetHubApi, setAssetHubApi] = useState<ApiPromise | null>(null);
const [peopleApi, setPeopleApi] = useState<ApiPromise | null>(null); const [peopleApi, setPeopleApi] = useState<ApiPromise | null>(null);
const [isApiReady, setIsApiReady] = useState(false); const [isApiReady, setIsApiReady] = useState(false);
const isApiReadyRef = useRef(false);
const [isApiInitializing, setIsApiInitializing] = useState(true);
const [isAssetHubReady, setIsAssetHubReady] = useState(false); const [isAssetHubReady, setIsAssetHubReady] = useState(false);
const [isPeopleReady, setIsPeopleReady] = useState(false); const [isPeopleReady, setIsPeopleReady] = useState(false);
const [accounts, setAccounts] = useState<InjectedAccountWithMeta[]>([]); const [accounts, setAccounts] = useState<InjectedAccountWithMeta[]>([]);
@@ -120,6 +123,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
const initApi = async () => { const initApi = async () => {
let lastError: unknown = null; let lastError: unknown = null;
setIsApiInitializing(true);
for (const currentEndpoint of FALLBACK_ENDPOINTS) { for (const currentEndpoint of FALLBACK_ENDPOINTS) {
try { try {
@@ -146,7 +150,9 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
await apiInstance.isReady; await apiInstance.isReady;
setApi(apiInstance); setApi(apiInstance);
isApiReadyRef.current = true;
setIsApiReady(true); setIsApiReady(true);
setIsApiInitializing(false);
setError(null); setError(null);
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
@@ -200,6 +206,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
} }
setError('Failed to connect to blockchain network. Please try again later.'); setError('Failed to connect to blockchain network. Please try again later.');
setIsApiReady(false); setIsApiReady(false);
setIsApiInitializing(false);
}; };
// Initialize Asset Hub API for PEZ token // Initialize Asset Hub API for PEZ token
@@ -489,8 +496,20 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
// Connect via WalletConnect v2 - returns pairing URI for QR code // Connect via WalletConnect v2 - returns pairing URI for QR code
const connectWalletConnect = async (): Promise<string> => { const connectWalletConnect = async (): Promise<string> => {
// Wait up to 30s for API to finish initializing instead of failing immediately
if (!isApiReady) {
await new Promise<void>((resolve, reject) => {
const deadline = Date.now() + 30_000;
const check = () => {
if (isApiReadyRef.current) return resolve();
if (Date.now() >= deadline) return reject(new Error('Blockchain connection timed out. Please refresh and try again.'));
setTimeout(check, 300);
};
check();
});
}
if (!api || !isApiReady) { if (!api || !isApiReady) {
throw new Error('API not ready. Please wait for blockchain connection.'); throw new Error('Blockchain connection failed. Please refresh and try again.');
} }
setError(null); setError(null);
@@ -581,6 +600,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
assetHubApi, assetHubApi,
peopleApi, peopleApi,
isApiReady, isApiReady,
isApiInitializing,
isAssetHubReady, isAssetHubReady,
isPeopleReady, isPeopleReady,
isConnected: isApiReady, isConnected: isApiReady,
+27 -5
View File
@@ -6,6 +6,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { identityToUUID } from '@shared/lib/identity';
import { Loader2, AlertTriangle, CheckCircle2 } from 'lucide-react'; import { Loader2, AlertTriangle, CheckCircle2 } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -51,8 +52,8 @@ export default function TelegramConnect() {
// Find user by telegram_id // Find user by telegram_id
const { data: userData, error: userError } = await supabase const { data: userData, error: userError } = await supabase
.from('users') .from('tg_users')
.select('id, telegram_id, wallet_address, username, first_name') .select('id, telegram_id, wallet_address, p2p_user_id')
.eq('telegram_id', parseInt(telegramId, 10)) .eq('telegram_id', parseInt(telegramId, 10))
.single(); .single();
@@ -62,11 +63,32 @@ export default function TelegramConnect() {
return; return;
} }
// Update wallet address if provided and different // Update wallet address and resolve p2p_user_id if not already set
const updates: Record<string, unknown> = {};
if (walletAddress && walletAddress !== userData.wallet_address) { if (walletAddress && walletAddress !== userData.wallet_address) {
updates.wallet_address = walletAddress;
}
if (!userData.p2p_user_id && walletAddress) {
// Try to find visa linked to this wallet (non-citizen users)
const { data: visa } = await supabase
.from('p2p_visa')
.select('visa_number')
.eq('wallet_address', walletAddress)
.eq('status', 'active')
.maybeSingle();
if (visa?.visa_number) {
updates.p2p_user_id = await identityToUUID(visa.visa_number);
}
// Welati (citizen) users: p2p_user_id will be set when they visit pwap/web P2P
// with their wallet connected (see P2PIdentityContext → linkTelegramP2PIdentity)
}
if (Object.keys(updates).length > 0) {
await supabase await supabase
.from('users') .from('tg_users')
.update({ wallet_address: walletAddress }) .update(updates)
.eq('id', userData.id); .eq('id', userData.id);
} }