Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 113 additions & 15 deletions examples/next-appkit-headless/components/ConnectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
'use client'

import { useState } from 'react'
import { useEffect, useRef, useState } from 'react'

import { AlertCircle } from 'lucide-react'
import { toast } from 'sonner'

import type { WalletItem } from '@reown/appkit'
import { type ChainNamespace } from '@reown/appkit/networks'
import { useAppKitWallets } from '@reown/appkit/react'
import type { ChainNamespace } from '@reown/appkit/networks'
import { CoreHelperUtil, type WalletConnectionError, useAppKitWallets } from '@reown/appkit/react'

import { NamespaceSelectionDialog } from '@/components/NamespaceSelectionDialog'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { cn } from '@/lib/utils'

Expand All @@ -16,13 +18,67 @@
import { ConnectContent } from './ConnectContent'
import { WalletConnectQRContent } from './WalletConnectQRContent'

export function ConnectCard({ className, ...props }: React.ComponentProps<'div'>) {

Check warning on line 21 in examples/next-appkit-headless/components/ConnectCard.tsx

View workflow job for this annotation

GitHub Actions / code_style (lint)

'props' is defined but never used
const { connect, wcUri, connectingWallet, resetWcUri } = useAppKitWallets()
const [connectionError, setConnectionError] = useState<WalletConnectionError | null>(null)
const previousWcErrorRef = useRef<boolean | undefined>(false)
const previousConnectingWalletRef = useRef<WalletItem | undefined>(undefined)
const errorShownRef = useRef<string | undefined>(undefined)

const { connect, wcUri, connectingWallet, resetWcUri, wcError } = useAppKitWallets({
onError: (error: WalletConnectionError) => {
setConnectionError(error)
// Show toast immediately when error callback is called
const errorKey = `${error.type}-${error.wallet?.id}-${Date.now()}`
if (errorShownRef.current !== errorKey) {
errorShownRef.current = errorKey
toast.error(error.message, {
duration: 5000
})
}
}
})

// Monitor wcError and connectingWallet changes to detect errors
useEffect(() => {
const previousWcError = previousWcErrorRef.current
const previousConnectingWallet = previousConnectingWalletRef.current

// If wcError became true and connectingWallet was cleared, it's likely a deep link failure
if (wcError && !previousWcError && previousConnectingWallet && !connectingWallet) {
// Error should be set via onError callback, but show toast as fallback
if (!connectionError && previousConnectingWallet) {
const errorKey = `fallback-${previousConnectingWallet.id}-${Date.now()}`
if (errorShownRef.current !== errorKey) {
errorShownRef.current = errorKey
toast.error(
`Unable to open ${previousConnectingWallet.name}. The app may not be installed on your device. Please install it from the App Store or Play Store, or try another wallet.`,
{
duration: 5000
}
)
}
}
}

// Reset error shown ref when connecting wallet changes
if (connectingWallet?.id !== previousConnectingWallet?.id) {
errorShownRef.current = undefined
}

previousWcErrorRef.current = wcError
previousConnectingWalletRef.current = connectingWallet
}, [wcError, connectingWallet, connectionError])

const [selectedWallet, setSelectedWallet] = useState<WalletItem | null>(null)
const [showWalletSearch, setShowWalletSearch] = useState(false)
const [isNamespaceDialogOpen, setIsNamespaceDialogOpen] = useState(false)

const showQRCode = wcUri && connectingWallet && !connectingWallet.isInjected
const isMobile = CoreHelperUtil.isMobile()
// Don't show QR code on mobile - deep linking is used instead
const showQRCode = !isMobile && wcUri && connectingWallet && !connectingWallet.isInjected
// Show error if deep link failed on mobile
const showDeepLinkError =
isMobile && wcError && connectionError?.type === 'DEEP_LINK_FAILED' && connectionError.wallet

function onOpenNamespaceDialog(item: WalletItem) {
setSelectedWallet(item)
Expand All @@ -31,15 +87,34 @@

async function onConnect(item: WalletItem, namespace?: ChainNamespace) {
setIsNamespaceDialogOpen(false)
await connect(item, namespace)
.then(() => {
setSelectedWallet(null)
toast.success('Connected wallet')
})
.catch(error => {
console.error(error)
toast.error('Failed to connect wallet')
})
setConnectionError(null) // Clear previous error when starting new connection
errorShownRef.current = undefined // Reset error shown ref

try {
await connect(item, namespace)
setSelectedWallet(null)
setConnectionError(null)
toast.success('Connected wallet')
} catch (error) {
// Error might be handled by onError callback, but show toast as fallback
// Check if error was already shown via onError callback
if (!connectionError) {
const errorMessage = error instanceof Error ? error.message : 'Failed to connect wallet'
const errorKey = `catch-${item.id}-${Date.now()}`
if (errorShownRef.current !== errorKey) {
errorShownRef.current = errorKey
toast.error(errorMessage, {
duration: 5000
})
}
}
console.error('Connection error:', error)
}
}

function handleDismissError() {
resetWcUri()
setConnectionError(null)
}

return (
Expand All @@ -60,7 +135,30 @@
{ 'flex p-0': showQRCode }
)}
>
<div className={cn('flex-1', showQRCode && 'border-r border-border')}>
<div className={cn('flex-1 flex flex-col', showQRCode && 'border-r border-border')}>
{/* Deep link error message */}
{showDeepLinkError && (
<div className="mx-6 mt-6 mb-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4">
<div className="flex items-start gap-3">
<AlertCircle className="size-5 text-destructive shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="font-semibold text-destructive mb-1">Connection Failed</h4>
<p className="text-sm text-muted-foreground mb-3">
{connectionError?.message ||
`Unable to open ${connectingWallet?.name}. The app may not be installed on your device. Please install it from the App Store or Play Store, or try another wallet.`}
</p>
<Button
variant="outline"
size="sm"
onClick={handleDismissError}
className="w-full sm:w-auto"
>
Dismiss
</Button>
</div>
</div>
</div>
)}
{showWalletSearch ? (
<AllWalletsContent onBack={() => setShowWalletSearch(false)} onConnect={onConnect} />
) : (
Expand Down
2 changes: 1 addition & 1 deletion examples/next-appkit-headless/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",
"incremental": true,
"plugins": [
{
Expand Down
1 change: 1 addition & 0 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { Emitter } from './src/utils/EmitterUtil.js'
export { PresetsUtil } from './src/utils/PresetsUtil.js'
export { ParseUtil } from './src/utils/ParseUtil.js'
export { ErrorUtil, UserRejectedRequestError } from './src/utils/ErrorUtil.js'
export type { ConnectionErrorType, ProviderRpcErrorCode } from './src/utils/ErrorUtil.js'
export {
SafeLocalStorage,
SafeLocalStorageKeys,
Expand Down
25 changes: 25 additions & 0 deletions packages/common/src/utils/ErrorUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export type ProviderRpcErrorCode =
| 5002 // User Rejected Methods
| 5000 // User Rejected

export type ConnectionErrorType =
| 'DEEP_LINK_FAILED'
| 'USER_REJECTED'
| 'CONNECTION_FAILED'

type RpcProviderError = {
message: string
code: ProviderRpcErrorCode
Expand All @@ -30,6 +35,11 @@ export const ErrorUtil = {
PROVIDER_RPC: 'ProviderRpcError',
USER_REJECTED_REQUEST: 'UserRejectedRequestError'
},
CONNECTION_ERROR_TYPE: {
DEEP_LINK_FAILED: 'DEEP_LINK_FAILED',
USER_REJECTED: 'USER_REJECTED',
CONNECTION_FAILED: 'CONNECTION_FAILED'
} as const,
isRpcProviderError(error: unknown): error is RpcProviderError {
try {
if (typeof error === 'object' && error !== null) {
Expand Down Expand Up @@ -71,6 +81,21 @@ export const ErrorUtil = {
}

return false
},
/**
* Gets a user-friendly error message for a given error.
* Uses existing error message if available, otherwise provides a default message.
*/
getErrorMessage(error: unknown, defaultMessage = 'An error occurred'): string {
if (error instanceof Error) {
return error.message || defaultMessage
}

if (ErrorUtil.isRpcProviderError(error)) {
return error.message || defaultMessage
}

return defaultMessage
}
}

Expand Down
Loading
Loading