create-awarizon-app 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +0 -0
- package/package.json +8 -8
- package/templates/expo/app/(tabs)/_layout.tsx +4 -3
- package/templates/expo/app/(tabs)/index.tsx +40 -64
- package/templates/expo/app/(tabs)/nft.tsx +142 -0
- package/templates/expo/app/(tabs)/wallet.tsx +127 -0
- package/templates/expo/babel.config.js +6 -0
- package/templates/expo/package.json +22 -23
- package/templates/expo-js/app/(tabs)/_layout.jsx +4 -3
- package/templates/expo-js/app/(tabs)/index.jsx +40 -66
- package/templates/expo-js/app/(tabs)/nft.jsx +141 -0
- package/templates/expo-js/app/(tabs)/wallet.jsx +128 -0
- package/templates/expo-js/package.json +19 -20
- package/templates/nextjs/app/layout.tsx +5 -1
- package/templates/nextjs/app/nft/page.tsx +140 -0
- package/templates/nextjs/app/page.tsx +37 -155
- package/templates/nextjs/app/wallet/page.tsx +116 -0
- package/templates/nextjs/components/Header.tsx +56 -0
- package/templates/nextjs/package.json +15 -16
- package/templates/nextjs-js/app/layout.jsx +5 -1
- package/templates/nextjs-js/app/nft/page.jsx +139 -0
- package/templates/nextjs-js/app/page.jsx +37 -128
- package/templates/nextjs-js/app/wallet/page.jsx +116 -0
- package/templates/nextjs-js/components/Header.jsx +56 -0
- package/templates/nextjs-js/package.json +11 -12
- package/templates/react/package.json +14 -15
- package/templates/react/src/App.tsx +88 -145
- package/templates/react/src/pages/NftPage.tsx +138 -0
- package/templates/react/src/pages/WalletPage.tsx +114 -0
- package/templates/react-js/package.json +11 -12
- package/templates/react-js/src/App.jsx +86 -118
- package/templates/react-js/src/pages/NftPage.jsx +137 -0
- package/templates/react-js/src/pages/WalletPage.jsx +114 -0
|
@@ -1,64 +1,41 @@
|
|
|
1
1
|
import { ScrollView, View, Text, StyleSheet, ActivityIndicator } from 'react-native'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{
|
|
9
|
-
{
|
|
10
|
-
{ name: 'decimals', type: 'function', inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' },
|
|
11
|
-
{ name: 'totalSupply', type: 'function', inputs: [], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
|
|
2
|
+
import { usePrices } from '@awarizon/react-native'
|
|
3
|
+
|
|
4
|
+
const TOKENS = [
|
|
5
|
+
{ symbol: 'ETH', name: 'Ethereum', color: '#627EEA' },
|
|
6
|
+
{ symbol: 'BTC', name: 'Bitcoin', color: '#F7931A' },
|
|
7
|
+
{ symbol: 'USDC', name: 'USD Coin', color: '#2775CA' },
|
|
8
|
+
{ symbol: 'BNB', name: 'BNB', color: '#F3BA2F' },
|
|
9
|
+
{ symbol: 'SOL', name: 'Solana', color: '#9945FF' },
|
|
12
10
|
]
|
|
13
11
|
|
|
14
|
-
function usdc(fn) {
|
|
15
|
-
return useReadContract({ address: USDC_BASE, abi: ERC20_ABI, method: fn })
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function Row({ label, value }) {
|
|
19
|
-
return (
|
|
20
|
-
<View style={s.row}>
|
|
21
|
-
<Text style={s.label}>{label}</Text>
|
|
22
|
-
{value === undefined
|
|
23
|
-
? <ActivityIndicator size="small" color="#6366f1" />
|
|
24
|
-
: <Text style={s.value}>{value}</Text>}
|
|
25
|
-
</View>
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
12
|
export default function HomeScreen() {
|
|
30
|
-
const {
|
|
31
|
-
const { data: symbol } = usdc('symbol')
|
|
32
|
-
const { data: decimals } = usdc('decimals')
|
|
33
|
-
const { data: totalSupply } = usdc('totalSupply')
|
|
34
|
-
|
|
35
|
-
const formatted =
|
|
36
|
-
totalSupply !== undefined && decimals !== undefined
|
|
37
|
-
? (Number(totalSupply) / 10 ** Number(decimals)).toLocaleString(undefined, { maximumFractionDigits: 2 })
|
|
38
|
-
: undefined
|
|
13
|
+
const { prices, isLoading } = usePrices(TOKENS.map(t => t.symbol))
|
|
39
14
|
|
|
40
15
|
return (
|
|
41
16
|
<ScrollView style={s.bg} contentContainerStyle={s.container}>
|
|
42
|
-
<View style={s.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
17
|
+
<View style={s.grid}>
|
|
18
|
+
{TOKENS.map(({ symbol, name, color }) => {
|
|
19
|
+
const price = prices[symbol]?.price
|
|
20
|
+
return (
|
|
21
|
+
<View key={symbol} style={s.card}>
|
|
22
|
+
<View style={[s.icon, { backgroundColor: color + '22', borderColor: color + '35' }]}>
|
|
23
|
+
<Text style={[s.iconText, { color }]}>{symbol.slice(0, 3)}</Text>
|
|
24
|
+
</View>
|
|
25
|
+
<Text style={s.symbol}>{symbol}</Text>
|
|
26
|
+
<Text style={s.name}>{name}</Text>
|
|
27
|
+
<View style={s.priceRow}>
|
|
28
|
+
{isLoading
|
|
29
|
+
? <ActivityIndicator size="small" color="#FFE500" />
|
|
30
|
+
: <Text style={s.price}>
|
|
31
|
+
{price != null
|
|
32
|
+
? `$${price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
33
|
+
: '—'}
|
|
34
|
+
</Text>}
|
|
35
|
+
</View>
|
|
36
|
+
</View>
|
|
37
|
+
)
|
|
38
|
+
})}
|
|
62
39
|
</View>
|
|
63
40
|
</ScrollView>
|
|
64
41
|
)
|
|
@@ -66,16 +43,13 @@ export default function HomeScreen() {
|
|
|
66
43
|
|
|
67
44
|
const s = StyleSheet.create({
|
|
68
45
|
bg: { flex: 1, backgroundColor: '#0a0a0a' },
|
|
69
|
-
container: { padding:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
value: { color: '#e5e7eb', fontSize: 13, fontWeight: '500', fontFamily: 'monospace' },
|
|
79
|
-
step: { color: '#9ca3af', fontSize: 13, lineHeight: 22, marginBottom: 4 },
|
|
80
|
-
code: { color: '#6366f1', fontFamily: 'monospace' },
|
|
46
|
+
container: { padding: 16, paddingBottom: 40 },
|
|
47
|
+
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
|
|
48
|
+
card: { width: '47%', backgroundColor: '#111827', borderWidth: 1, borderColor: '#1f2937', borderRadius: 16, padding: 16 },
|
|
49
|
+
icon: { width: 40, height: 40, borderRadius: 20, borderWidth: 1, alignItems: 'center', justifyContent: 'center', marginBottom: 12 },
|
|
50
|
+
iconText: { fontSize: 10, fontWeight: '700', letterSpacing: 0.5 },
|
|
51
|
+
symbol: { color: '#6b7280', fontSize: 11, fontWeight: '600', marginBottom: 2 },
|
|
52
|
+
name: { color: '#374151', fontSize: 11, marginBottom: 10 },
|
|
53
|
+
priceRow: { minHeight: 22, justifyContent: 'center' },
|
|
54
|
+
price: { color: '#f9fafb', fontSize: 16, fontWeight: '600', letterSpacing: -0.3 },
|
|
81
55
|
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { ScrollView, View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native'
|
|
3
|
+
import { contract, useRead, useSDK } from '@awarizon/react-native'
|
|
4
|
+
import { ERC721_ABI } from '@awarizon/web3'
|
|
5
|
+
|
|
6
|
+
// Replace with your ERC-721 contract address on your configured chain.
|
|
7
|
+
const NFT_ADDRESS = '0x0000000000000000000000000000000000000000'
|
|
8
|
+
const NFT = contract(NFT_ADDRESS, ERC721_ABI)
|
|
9
|
+
|
|
10
|
+
function Row({ label, value }) {
|
|
11
|
+
return (
|
|
12
|
+
<View style={s.row}>
|
|
13
|
+
<Text style={s.label}>{label}</Text>
|
|
14
|
+
{value === undefined
|
|
15
|
+
? <ActivityIndicator size="small" color="#FFE500" />
|
|
16
|
+
: <Text style={s.value}>{value}</Text>}
|
|
17
|
+
</View>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function NftScreen() {
|
|
22
|
+
const { data: name, isLoading: l1, error: e1 } = useRead(NFT.name())
|
|
23
|
+
const { data: symbol, isLoading: l2, error: e2 } = useRead(NFT.symbol())
|
|
24
|
+
const awarizon = useSDK()
|
|
25
|
+
|
|
26
|
+
const [tokenId, setTokenId] = useState('')
|
|
27
|
+
const [owner, setOwner] = useState(null)
|
|
28
|
+
const [uri, setUri] = useState(null)
|
|
29
|
+
const [lookupLoading, setLookupLoading] = useState(false)
|
|
30
|
+
const [lookupError, setLookupError] = useState(null)
|
|
31
|
+
|
|
32
|
+
const collectionError = e1?.message ?? e2?.message ?? null
|
|
33
|
+
|
|
34
|
+
async function handleLookup() {
|
|
35
|
+
if (!tokenId) return
|
|
36
|
+
setLookupLoading(true)
|
|
37
|
+
setLookupError(null)
|
|
38
|
+
setOwner(null)
|
|
39
|
+
setUri(null)
|
|
40
|
+
try {
|
|
41
|
+
const id = BigInt(tokenId)
|
|
42
|
+
const nft = await awarizon.contract({ address: NFT_ADDRESS, abi: ERC721_ABI })
|
|
43
|
+
const [o, u] = await Promise.all([nft.ownerOf(id), nft.tokenURI(id)])
|
|
44
|
+
setOwner(String(o))
|
|
45
|
+
setUri(String(u))
|
|
46
|
+
} catch (e) {
|
|
47
|
+
setLookupError(e instanceof Error ? e.message : 'Lookup failed')
|
|
48
|
+
} finally {
|
|
49
|
+
setLookupLoading(false)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ScrollView style={s.bg} contentContainerStyle={s.container}>
|
|
55
|
+
<View style={s.hero}>
|
|
56
|
+
<Text style={s.badge}>ERC-721</Text>
|
|
57
|
+
<Text style={s.heading}>NFT Collection</Text>
|
|
58
|
+
<Text style={s.sub}>
|
|
59
|
+
Replace NFT_ADDRESS in this file with your ERC-721 contract address.
|
|
60
|
+
</Text>
|
|
61
|
+
</View>
|
|
62
|
+
|
|
63
|
+
{collectionError && (
|
|
64
|
+
<View style={s.errorBox}>
|
|
65
|
+
<Text style={s.errorText}>{collectionError}</Text>
|
|
66
|
+
</View>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<View style={s.card}>
|
|
70
|
+
<Text style={s.cardTitle}>Collection Info</Text>
|
|
71
|
+
<Text style={s.hint}>{NFT_ADDRESS.slice(0, 6)}…{NFT_ADDRESS.slice(-4)}</Text>
|
|
72
|
+
<Row label="name()" value={l1 ? undefined : name ?? '—'} />
|
|
73
|
+
<Row label="symbol()" value={l2 ? undefined : symbol ?? '—'} />
|
|
74
|
+
</View>
|
|
75
|
+
|
|
76
|
+
<View style={s.card}>
|
|
77
|
+
<Text style={s.cardTitle}>Token Lookup</Text>
|
|
78
|
+
<View style={s.inputRow}>
|
|
79
|
+
<TextInput
|
|
80
|
+
style={s.input}
|
|
81
|
+
value={tokenId}
|
|
82
|
+
onChangeText={setTokenId}
|
|
83
|
+
placeholder="Token ID"
|
|
84
|
+
placeholderTextColor="#4b5563"
|
|
85
|
+
keyboardType="numeric"
|
|
86
|
+
/>
|
|
87
|
+
<TouchableOpacity
|
|
88
|
+
style={[s.button, (!tokenId || lookupLoading) && s.buttonDisabled]}
|
|
89
|
+
onPress={handleLookup}
|
|
90
|
+
disabled={!tokenId || lookupLoading}
|
|
91
|
+
>
|
|
92
|
+
<Text style={s.buttonText}>{lookupLoading ? 'Loading…' : 'Look up'}</Text>
|
|
93
|
+
</TouchableOpacity>
|
|
94
|
+
</View>
|
|
95
|
+
{lookupError && <Text style={s.errorText}>{lookupError}</Text>}
|
|
96
|
+
{owner !== null && (
|
|
97
|
+
<Row label="ownerOf()" value={`${owner.slice(0, 6)}…${owner.slice(-4)}`} />
|
|
98
|
+
)}
|
|
99
|
+
{uri !== null && (
|
|
100
|
+
<View>
|
|
101
|
+
<Text style={s.hint}>tokenURI()</Text>
|
|
102
|
+
<Text style={s.uriText}>{uri}</Text>
|
|
103
|
+
</View>
|
|
104
|
+
)}
|
|
105
|
+
</View>
|
|
106
|
+
|
|
107
|
+
<View style={s.card}>
|
|
108
|
+
<Text style={s.cardTitle}>Usage Notes</Text>
|
|
109
|
+
<Text style={s.step}>1. Replace NFT_ADDRESS at the top of this file</Text>
|
|
110
|
+
<Text style={s.step}>2. No ABI needed — ERC721_ABI is built into the SDK</Text>
|
|
111
|
+
<Text style={s.step}>3. Enter a token ID and tap Look up to call <Text style={s.code}>ownerOf</Text> and <Text style={s.code}>tokenURI</Text></Text>
|
|
112
|
+
<Text style={s.step}>4. Use <Text style={s.code}>awarizon.contract()</Text> for on-demand reads in handlers</Text>
|
|
113
|
+
</View>
|
|
114
|
+
</ScrollView>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const s = StyleSheet.create({
|
|
119
|
+
bg: { flex: 1, backgroundColor: '#0a0a0a' },
|
|
120
|
+
container: { padding: 20, paddingBottom: 40 },
|
|
121
|
+
hero: { marginBottom: 24 },
|
|
122
|
+
badge: { color: '#FFE500', fontSize: 12, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 8 },
|
|
123
|
+
heading: { color: '#fff', fontSize: 28, fontWeight: '800', marginBottom: 6 },
|
|
124
|
+
sub: { color: '#9ca3af', fontSize: 15, lineHeight: 22 },
|
|
125
|
+
card: { backgroundColor: '#111', borderWidth: 1, borderColor: '#1f1f1f', borderRadius: 12, padding: 16, marginBottom: 16 },
|
|
126
|
+
cardTitle: { color: '#fff', fontWeight: '700', fontSize: 15, marginBottom: 12 },
|
|
127
|
+
hint: { color: '#4b5563', fontSize: 11, marginBottom: 8 },
|
|
128
|
+
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#1f1f1f' },
|
|
129
|
+
label: { color: '#9ca3af', fontSize: 13 },
|
|
130
|
+
value: { color: '#e5e7eb', fontSize: 13, fontWeight: '500', fontFamily: 'monospace' },
|
|
131
|
+
inputRow: { flexDirection: 'row', gap: 8, marginBottom: 12 },
|
|
132
|
+
input: { flex: 1, backgroundColor: '#1a1a1a', borderWidth: 1, borderColor: '#333', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, color: '#e5e7eb', fontFamily: 'monospace', fontSize: 13 },
|
|
133
|
+
button: { backgroundColor: '#FFE500', borderRadius: 8, paddingHorizontal: 16, paddingVertical: 8, justifyContent: 'center' },
|
|
134
|
+
buttonDisabled: { opacity: 0.4 },
|
|
135
|
+
buttonText: { color: '#000', fontWeight: '700', fontSize: 12 },
|
|
136
|
+
errorBox: { backgroundColor: 'rgba(239,68,68,0.1)', borderWidth: 1, borderColor: 'rgba(239,68,68,0.2)', borderRadius: 8, padding: 12, marginBottom: 16 },
|
|
137
|
+
errorText: { color: '#f87171', fontSize: 12, fontFamily: 'monospace' },
|
|
138
|
+
uriText: { color: '#e5e7eb', fontSize: 12, fontFamily: 'monospace', marginTop: 4 },
|
|
139
|
+
step: { color: '#9ca3af', fontSize: 13, lineHeight: 22, marginBottom: 4 },
|
|
140
|
+
code: { color: '#FFE500', fontFamily: 'monospace' },
|
|
141
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { ScrollView, View, Text, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native'
|
|
3
|
+
import { useSDK, useRNWallet, usePrices } from '@awarizon/react-native'
|
|
4
|
+
|
|
5
|
+
const SYMBOLS = ['ETH', 'BTC', 'USDC']
|
|
6
|
+
|
|
7
|
+
function Row({ label, value, loading = false }) {
|
|
8
|
+
return (
|
|
9
|
+
<View style={s.row}>
|
|
10
|
+
<Text style={s.label}>{label}</Text>
|
|
11
|
+
{loading
|
|
12
|
+
? <ActivityIndicator size="small" color="#FFE500" />
|
|
13
|
+
: <Text style={s.value}>{value ?? '—'}</Text>}
|
|
14
|
+
</View>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function WalletScreen() {
|
|
19
|
+
const awarizon = useSDK()
|
|
20
|
+
const { address, isConnected, isLoading: walletLoading, create } = useRNWallet({ awarizon })
|
|
21
|
+
const { prices, isLoading: pricesLoading } = usePrices(SYMBOLS)
|
|
22
|
+
|
|
23
|
+
const [nativeBalance, setNativeBalance] = useState(null)
|
|
24
|
+
const [balanceLoading, setBalanceLoading] = useState(false)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!address) { setNativeBalance(null); return }
|
|
28
|
+
setBalanceLoading(true)
|
|
29
|
+
awarizon.publicClient
|
|
30
|
+
.getBalance({ address })
|
|
31
|
+
.then(bal => {
|
|
32
|
+
const eth = Number(bal) / 1e18
|
|
33
|
+
setNativeBalance(eth.toFixed(6))
|
|
34
|
+
})
|
|
35
|
+
.catch(() => setNativeBalance(null))
|
|
36
|
+
.finally(() => setBalanceLoading(false))
|
|
37
|
+
}, [address, awarizon])
|
|
38
|
+
|
|
39
|
+
const fmtPrice = (sym) =>
|
|
40
|
+
prices[sym]?.price != null
|
|
41
|
+
? `$${prices[sym].price.toLocaleString(undefined, { maximumFractionDigits: 2 })}`
|
|
42
|
+
: '—'
|
|
43
|
+
|
|
44
|
+
const ethFiatValue =
|
|
45
|
+
nativeBalance != null && prices['ETH']?.price != null
|
|
46
|
+
? `$${(parseFloat(nativeBalance) * prices['ETH'].price).toLocaleString(undefined, { maximumFractionDigits: 2 })}`
|
|
47
|
+
: null
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<ScrollView style={s.bg} contentContainerStyle={s.container}>
|
|
51
|
+
<View style={s.hero}>
|
|
52
|
+
<Text style={s.badge}>Portfolio</Text>
|
|
53
|
+
<Text style={s.heading}>Wallet</Text>
|
|
54
|
+
<Text style={s.sub}>
|
|
55
|
+
Native balance and market prices via <Text style={s.code}>useRNWallet</Text> and <Text style={s.code}>usePrices</Text>.
|
|
56
|
+
</Text>
|
|
57
|
+
</View>
|
|
58
|
+
|
|
59
|
+
{!isConnected ? (
|
|
60
|
+
<View style={s.card}>
|
|
61
|
+
<Text style={s.cardTitle}>No Wallet</Text>
|
|
62
|
+
<Text style={s.sub}>Create a wallet to view your balance and portfolio.</Text>
|
|
63
|
+
<TouchableOpacity
|
|
64
|
+
style={[s.button, walletLoading && s.buttonDisabled]}
|
|
65
|
+
onPress={create}
|
|
66
|
+
disabled={walletLoading}
|
|
67
|
+
>
|
|
68
|
+
{walletLoading
|
|
69
|
+
? <ActivityIndicator size="small" color="#000" />
|
|
70
|
+
: <Text style={s.buttonText}>Create Wallet</Text>}
|
|
71
|
+
</TouchableOpacity>
|
|
72
|
+
</View>
|
|
73
|
+
) : (
|
|
74
|
+
<>
|
|
75
|
+
<View style={s.card}>
|
|
76
|
+
<Text style={s.cardTitle}>Account</Text>
|
|
77
|
+
<Row label="Address" value={address ? `${address.slice(0, 6)}…${address.slice(-4)}` : undefined} />
|
|
78
|
+
</View>
|
|
79
|
+
|
|
80
|
+
<View style={s.card}>
|
|
81
|
+
<Text style={s.cardTitle}>Balance</Text>
|
|
82
|
+
<Row
|
|
83
|
+
label="Native"
|
|
84
|
+
value={nativeBalance != null ? `${nativeBalance} ETH` : undefined}
|
|
85
|
+
loading={balanceLoading}
|
|
86
|
+
/>
|
|
87
|
+
<Row
|
|
88
|
+
label="Est. Value"
|
|
89
|
+
value={ethFiatValue ?? undefined}
|
|
90
|
+
loading={balanceLoading || pricesLoading}
|
|
91
|
+
/>
|
|
92
|
+
<Text style={s.hint}>via publicClient.getBalance · ETH × market price</Text>
|
|
93
|
+
</View>
|
|
94
|
+
|
|
95
|
+
<View style={s.card}>
|
|
96
|
+
<Text style={s.cardTitle}>Market Prices</Text>
|
|
97
|
+
{pricesLoading
|
|
98
|
+
? <ActivityIndicator size="small" color="#FFE500" style={{ marginVertical: 8 }} />
|
|
99
|
+
: SYMBOLS.map(sym => (
|
|
100
|
+
<Row key={sym} label={sym} value={fmtPrice(sym)} />
|
|
101
|
+
))
|
|
102
|
+
}
|
|
103
|
+
<Text style={s.hint}>via usePrices · Chainlink + CoinGecko fallback</Text>
|
|
104
|
+
</View>
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
</ScrollView>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const s = StyleSheet.create({
|
|
112
|
+
bg: { flex: 1, backgroundColor: '#0a0a0a' },
|
|
113
|
+
container: { padding: 20, paddingBottom: 40 },
|
|
114
|
+
hero: { marginBottom: 24 },
|
|
115
|
+
badge: { color: '#FFE500', fontSize: 12, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 8 },
|
|
116
|
+
heading: { color: '#fff', fontSize: 28, fontWeight: '800', marginBottom: 6 },
|
|
117
|
+
sub: { color: '#9ca3af', fontSize: 15, lineHeight: 22 },
|
|
118
|
+
card: { backgroundColor: '#111', borderWidth: 1, borderColor: '#1f1f1f', borderRadius: 12, padding: 16, marginBottom: 16 },
|
|
119
|
+
cardTitle: { color: '#fff', fontWeight: '700', fontSize: 15, marginBottom: 12 },
|
|
120
|
+
hint: { color: '#4b5563', fontSize: 11, marginTop: 8 },
|
|
121
|
+
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#1f1f1f' },
|
|
122
|
+
label: { color: '#9ca3af', fontSize: 13 },
|
|
123
|
+
value: { color: '#e5e7eb', fontSize: 13, fontWeight: '500', fontFamily: 'monospace' },
|
|
124
|
+
button: { backgroundColor: '#FFE500', borderRadius: 8, paddingHorizontal: 16, paddingVertical: 10, alignItems: 'center', marginTop: 16 },
|
|
125
|
+
buttonDisabled: { opacity: 0.4 },
|
|
126
|
+
buttonText: { color: '#000', fontWeight: '700', fontSize: 13 },
|
|
127
|
+
code: { color: '#FFE500', fontFamily: 'monospace' },
|
|
128
|
+
})
|
|
@@ -3,32 +3,31 @@
|
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"main": "expo-router/entry",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"start":
|
|
6
|
+
"start": "expo start",
|
|
7
7
|
"android": "expo start --android",
|
|
8
|
-
"ios":
|
|
9
|
-
"web":
|
|
8
|
+
"ios": "expo start --ios",
|
|
9
|
+
"web": "expo start --web"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@awarizon/react-native": "^1.2.
|
|
13
|
-
"@awarizon/web3":
|
|
14
|
-
"@expo/vector-icons":
|
|
15
|
-
"expo":
|
|
16
|
-
"expo-constants":
|
|
17
|
-
"expo-font":
|
|
18
|
-
"expo-linking":
|
|
19
|
-
"expo-router":
|
|
20
|
-
"expo-secure-store":
|
|
21
|
-
"expo-splash-screen":
|
|
22
|
-
"expo-status-bar":
|
|
23
|
-
"expo-system-ui":
|
|
24
|
-
"react":
|
|
25
|
-
"react-dom":
|
|
26
|
-
"react-native":
|
|
12
|
+
"@awarizon/react-native": "^1.2.1",
|
|
13
|
+
"@awarizon/web3": "^1.3.5",
|
|
14
|
+
"@expo/vector-icons": "^14.0.2",
|
|
15
|
+
"expo": "~51.0.0",
|
|
16
|
+
"expo-constants": "~16.0.2",
|
|
17
|
+
"expo-font": "~12.0.9",
|
|
18
|
+
"expo-linking": "~6.3.1",
|
|
19
|
+
"expo-router": "~3.5.17",
|
|
20
|
+
"expo-secure-store": "~13.0.2",
|
|
21
|
+
"expo-splash-screen": "~0.27.5",
|
|
22
|
+
"expo-status-bar": "~1.12.1",
|
|
23
|
+
"expo-system-ui": "~3.0.7",
|
|
24
|
+
"react": "18.2.0",
|
|
25
|
+
"react-dom": "18.2.0",
|
|
26
|
+
"react-native": "0.74.5",
|
|
27
27
|
"react-native-safe-area-context": "4.10.5",
|
|
28
|
-
"react-native-screens":
|
|
28
|
+
"react-native-screens": "3.31.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@babel/core": "^7.24.0"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { Providers } from '@/components/Providers'
|
|
3
|
+
import { Header } from '@/components/Header'
|
|
3
4
|
import './globals.css'
|
|
4
5
|
|
|
5
6
|
export const metadata: Metadata = {
|
|
@@ -11,7 +12,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
11
12
|
return (
|
|
12
13
|
<html lang="en">
|
|
13
14
|
<body>
|
|
14
|
-
<Providers>
|
|
15
|
+
<Providers>
|
|
16
|
+
<Header />
|
|
17
|
+
{children}
|
|
18
|
+
</Providers>
|
|
15
19
|
</body>
|
|
16
20
|
</html>
|
|
17
21
|
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useNFT } from '@awarizon/react'
|
|
5
|
+
import type { Address } from '@awarizon/web3'
|
|
6
|
+
import { Badge } from '@/components/ui/Badge'
|
|
7
|
+
import { Button } from '@/components/ui/Button'
|
|
8
|
+
import { Card } from '@/components/ui/Card'
|
|
9
|
+
import { Skeleton } from '@/components/ui/Skeleton'
|
|
10
|
+
|
|
11
|
+
// Replace with your ERC-721 contract address on your configured chain.
|
|
12
|
+
const NFT_ADDRESS = '0x0000000000000000000000000000000000000000' as Address
|
|
13
|
+
|
|
14
|
+
function DataRow({ label, value, loading }: { label: string; value?: string; loading: boolean }) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex items-center justify-between py-3 border-b border-zinc-800/50 last:border-0">
|
|
17
|
+
<code className="text-[11px] text-yellow-400/70 font-mono">{label}</code>
|
|
18
|
+
{loading
|
|
19
|
+
? <Skeleton className="h-3.5 w-28" />
|
|
20
|
+
: <span className="font-mono text-xs text-zinc-300">{value ?? '—'}</span>}
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function NftPage() {
|
|
26
|
+
const { name, symbol, isLoading, error, ownerOf, tokenURI } = useNFT(NFT_ADDRESS)
|
|
27
|
+
|
|
28
|
+
const [tokenId, setTokenId] = useState('')
|
|
29
|
+
const [owner, setOwner] = useState<string | null>(null)
|
|
30
|
+
const [uri, setUri] = useState<string | null>(null)
|
|
31
|
+
const [lookupLoading, setLookupLoading] = useState(false)
|
|
32
|
+
const [lookupError, setLookupError] = useState<string | null>(null)
|
|
33
|
+
|
|
34
|
+
async function handleLookup() {
|
|
35
|
+
if (!tokenId) return
|
|
36
|
+
setLookupLoading(true)
|
|
37
|
+
setLookupError(null)
|
|
38
|
+
setOwner(null)
|
|
39
|
+
setUri(null)
|
|
40
|
+
try {
|
|
41
|
+
const id = BigInt(tokenId)
|
|
42
|
+
const [o, u] = await Promise.all([ownerOf(id), tokenURI(id)])
|
|
43
|
+
setOwner(o)
|
|
44
|
+
setUri(u)
|
|
45
|
+
} catch (e) {
|
|
46
|
+
setLookupError(e instanceof Error ? e.message : 'Lookup failed')
|
|
47
|
+
} finally {
|
|
48
|
+
setLookupLoading(false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<main className="max-w-4xl mx-auto px-6 py-12">
|
|
54
|
+
|
|
55
|
+
{/* ── Hero ───────────────────────────────────────── */}
|
|
56
|
+
<div className="mb-10">
|
|
57
|
+
<Badge variant="outline" className="mb-5">ERC-721</Badge>
|
|
58
|
+
<h1 className="text-4xl font-bold tracking-tight text-white mb-3">NFT Collection</h1>
|
|
59
|
+
<p className="text-zinc-400 text-base max-w-lg leading-relaxed">
|
|
60
|
+
Zero-ABI reads from any ERC-721 contract using{' '}
|
|
61
|
+
<code className="font-mono text-[11px] text-yellow-400/80">useNFT</code>.
|
|
62
|
+
Replace <code className="font-mono text-[11px] text-yellow-400/80">NFT_ADDRESS</code> at the top of this file.
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{error && (
|
|
67
|
+
<div className="mb-4 px-4 py-3 bg-red-500/10 border border-red-500/20 rounded-xl">
|
|
68
|
+
<code className="font-mono text-xs text-red-400">{error.message}</code>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{/* ── Cards ──────────────────────────────────────── */}
|
|
73
|
+
<div className="grid md:grid-cols-2 gap-4 mb-4">
|
|
74
|
+
|
|
75
|
+
<Card>
|
|
76
|
+
<div className="mb-4">
|
|
77
|
+
<p className="font-mono text-[9px] tracking-widest text-zinc-600 uppercase mb-1.5">Collection Info</p>
|
|
78
|
+
<code className="font-mono text-xs text-zinc-500">
|
|
79
|
+
{NFT_ADDRESS.slice(0, 6)}…{NFT_ADDRESS.slice(-4)}
|
|
80
|
+
</code>
|
|
81
|
+
</div>
|
|
82
|
+
<DataRow label="name()" value={name ?? undefined} loading={isLoading} />
|
|
83
|
+
<DataRow label="symbol()" value={symbol ?? undefined} loading={isLoading} />
|
|
84
|
+
</Card>
|
|
85
|
+
|
|
86
|
+
<Card>
|
|
87
|
+
<p className="font-mono text-[9px] tracking-widest text-zinc-600 uppercase mb-4">Token Lookup</p>
|
|
88
|
+
<div className="flex gap-2 mb-4">
|
|
89
|
+
<input
|
|
90
|
+
type="number"
|
|
91
|
+
min="0"
|
|
92
|
+
value={tokenId}
|
|
93
|
+
onChange={e => setTokenId(e.target.value)}
|
|
94
|
+
placeholder="Token ID"
|
|
95
|
+
className="flex-1 bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 font-mono text-xs text-zinc-200 placeholder-zinc-600 outline-none focus:border-yellow-400/40"
|
|
96
|
+
/>
|
|
97
|
+
<Button onClick={handleLookup} disabled={!tokenId || lookupLoading}>
|
|
98
|
+
{lookupLoading ? 'Loading…' : 'Look up'}
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
{lookupError && (
|
|
102
|
+
<p className="font-mono text-[11px] text-red-400 mb-3">{lookupError}</p>
|
|
103
|
+
)}
|
|
104
|
+
{owner !== null && (
|
|
105
|
+
<DataRow
|
|
106
|
+
label="ownerOf()"
|
|
107
|
+
value={`${owner.slice(0, 6)}…${owner.slice(-4)}`}
|
|
108
|
+
loading={false}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
{uri !== null && (
|
|
112
|
+
<div className="pt-3">
|
|
113
|
+
<p className="font-mono text-[9px] tracking-widest text-zinc-600 uppercase mb-1.5">tokenURI()</p>
|
|
114
|
+
<p className="font-mono text-xs text-zinc-300 break-all">{uri}</p>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</Card>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* ── Notes ──────────────────────────────────────── */}
|
|
121
|
+
<Card>
|
|
122
|
+
<p className="font-mono text-[9px] tracking-widest text-zinc-600 uppercase mb-4">Usage Notes</p>
|
|
123
|
+
<ul className="space-y-3">
|
|
124
|
+
{[
|
|
125
|
+
['01', 'Replace NFT_ADDRESS with any ERC-721 contract on your configured chain'],
|
|
126
|
+
['02', 'useNFT requires no ABI — the SDK uses the built-in ERC-721 interface'],
|
|
127
|
+
['03', 'ownerOf(tokenId) returns the owner address of a specific token'],
|
|
128
|
+
['04', 'tokenURI(tokenId) returns the metadata URI (IPFS, Arweave, HTTP, etc.)'],
|
|
129
|
+
['05', 'balanceOf(address) counts how many tokens a given address holds'],
|
|
130
|
+
].map(([n, s]) => (
|
|
131
|
+
<li key={n} className="flex items-start gap-3">
|
|
132
|
+
<span className="font-mono text-[10px] text-yellow-400/50 mt-0.5 shrink-0 select-none">{n}</span>
|
|
133
|
+
<span className="text-sm text-zinc-400 leading-relaxed">{s}</span>
|
|
134
|
+
</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
</Card>
|
|
138
|
+
</main>
|
|
139
|
+
)
|
|
140
|
+
}
|