@akta/algomd-rn 0.0.1-canary
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/package.json +46 -0
- package/src/components/ASA.tsx +221 -0
- package/src/components/ASASearch.tsx +79 -0
- package/src/components/Account.tsx +150 -0
- package/src/components/AccountSearch.tsx +75 -0
- package/src/components/AuctionListing.tsx +220 -0
- package/src/components/NFDProfile.tsx +173 -0
- package/src/components/NFDSearch.tsx +81 -0
- package/src/components/NFTListing.tsx +181 -0
- package/src/components/NFTSearch.tsx +68 -0
- package/src/components/Poll.tsx +234 -0
- package/src/components/PollSearch.tsx +79 -0
- package/src/components/RaffleListing.tsx +204 -0
- package/src/components/SelfFetching.tsx +358 -0
- package/src/components/TradeOffer.tsx +245 -0
- package/src/components/TradeSearch.tsx +103 -0
- package/src/components/TransactionDetails.tsx +242 -0
- package/src/components/TransactionSearch.tsx +102 -0
- package/src/components/index.ts +30 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/useAlgomdData.ts +445 -0
- package/src/hooks/useAlgorandClient.ts +39 -0
- package/src/index.ts +83 -0
- package/src/provider/AlgomdProvider.tsx +45 -0
- package/src/provider/context.ts +18 -0
- package/src/provider/imageResolver.ts +12 -0
- package/src/provider/index.ts +5 -0
- package/src/provider/networks.ts +23 -0
- package/src/provider/types.ts +17 -0
- package/src/types/algorand.ts +205 -0
- package/src/types/index.ts +1 -0
- package/src/ui/CopyButton.tsx +40 -0
- package/src/ui/DataStates.tsx +41 -0
- package/src/ui/ProgressBar.tsx +42 -0
- package/src/ui/SearchSheet.tsx +134 -0
- package/src/ui/SizeContainer.tsx +25 -0
- package/src/ui/StatusBadge.tsx +51 -0
- package/src/ui/index.ts +6 -0
- package/src/utils/format.ts +70 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/search.ts +65 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@akta/algomd-rn",
|
|
3
|
+
"version": "0.0.1-canary",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"types": "src/index.ts",
|
|
6
|
+
"react-native": "src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"compile": "tsc --noEmit -p . --pretty",
|
|
9
|
+
"lint": "eslint src/ --fix"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"clsx": ">=2.0.0",
|
|
19
|
+
"expo-clipboard": ">=8.0.0",
|
|
20
|
+
"expo-haptics": ">=15.0.0",
|
|
21
|
+
"expo-image": ">=3.0.0",
|
|
22
|
+
"expo-linking": ">=8.0.0",
|
|
23
|
+
"nativewind": ">=4.0.0",
|
|
24
|
+
"react": ">=19.0.0",
|
|
25
|
+
"react-native": ">=0.81.0",
|
|
26
|
+
"react-native-gesture-handler": ">=2.0.0",
|
|
27
|
+
"react-native-reanimated": ">=4.0.0",
|
|
28
|
+
"react-native-svg": ">=15.0.0",
|
|
29
|
+
"@gorhom/bottom-sheet": ">=5.0.0",
|
|
30
|
+
"@shopify/flash-list": ">=2.0.0",
|
|
31
|
+
"@tanstack/react-query": ">=5",
|
|
32
|
+
"algosdk": ">=3.0.0",
|
|
33
|
+
"@algorandfoundation/algokit-utils": ">=9.0.0",
|
|
34
|
+
"@akta/sdk": "^0.0.1"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"algosdk": { "optional": true },
|
|
38
|
+
"@algorandfoundation/algokit-utils": { "optional": true },
|
|
39
|
+
"@akta/sdk": { "optional": true },
|
|
40
|
+
"@tanstack/react-query": { "optional": true }
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/react": "^19.1.0",
|
|
44
|
+
"typescript": "~5.9.2"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Pressable } from 'react-native';
|
|
3
|
+
import { Image } from 'expo-image';
|
|
4
|
+
import Animated, { FadeInDown } from 'react-native-reanimated';
|
|
5
|
+
import Svg, { Path } from 'react-native-svg';
|
|
6
|
+
import type { ASA as ASAType, ComponentSize } from '../types/algorand';
|
|
7
|
+
import { formatNumber, formatAssetAmount, formatRelativeTime } from '../utils/format';
|
|
8
|
+
import { CopyButton } from '../ui/CopyButton';
|
|
9
|
+
import { SizeContainer } from '../ui/SizeContainer';
|
|
10
|
+
|
|
11
|
+
function ShieldIcon({ size = 16, color = '#60a5fa' }: { size?: number; color?: string }) {
|
|
12
|
+
return (
|
|
13
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
14
|
+
<Path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
|
|
15
|
+
</Svg>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function LockIcon({ size = 16, color = '#fb923c' }: { size?: number; color?: string }) {
|
|
20
|
+
return (
|
|
21
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
22
|
+
<Path d="M16 10V7a4 4 0 0 0-8 0v3" />
|
|
23
|
+
<Path d="M5 10h14a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V11a1 1 0 0 1 1-1Z" />
|
|
24
|
+
</Svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function UnlockIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
29
|
+
return (
|
|
30
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
31
|
+
<Path d="M8 10V7a4 4 0 0 1 8 0" />
|
|
32
|
+
<Path d="M5 10h14a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V11a1 1 0 0 1 1-1Z" />
|
|
33
|
+
</Svg>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function CalendarIcon({ size = 12, color = '#71717a' }: { size?: number; color?: string }) {
|
|
38
|
+
return (
|
|
39
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
40
|
+
<Path d="M8 2v4" />
|
|
41
|
+
<Path d="M16 2v4" />
|
|
42
|
+
<Path d="M3 10h18" />
|
|
43
|
+
<Path d="M21 8V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" />
|
|
44
|
+
</Svg>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ASAProps {
|
|
49
|
+
data: ASAType;
|
|
50
|
+
showDetails?: boolean;
|
|
51
|
+
compact?: boolean;
|
|
52
|
+
size?: ComponentSize;
|
|
53
|
+
className?: string;
|
|
54
|
+
imageUrl?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ASAComponent({
|
|
58
|
+
data: asset,
|
|
59
|
+
showDetails = false,
|
|
60
|
+
compact = false,
|
|
61
|
+
size = 'full',
|
|
62
|
+
className,
|
|
63
|
+
imageUrl,
|
|
64
|
+
}: ASAProps) {
|
|
65
|
+
const isNFT = asset.total === 1 && asset.decimals === 0;
|
|
66
|
+
|
|
67
|
+
if (compact) {
|
|
68
|
+
return (
|
|
69
|
+
<View className={`flex-row items-center gap-1.5 px-2 py-1 rounded-full bg-zinc-800/60 ${className ?? ''}`}>
|
|
70
|
+
{imageUrl && (
|
|
71
|
+
<View className="w-4 h-4 rounded-full overflow-hidden bg-zinc-700">
|
|
72
|
+
<Image source={{ uri: imageUrl }} style={{ width: 16, height: 16 }} contentFit="cover" />
|
|
73
|
+
</View>
|
|
74
|
+
)}
|
|
75
|
+
{asset.verified && <ShieldIcon size={12} color="#60a5fa" />}
|
|
76
|
+
<Text className="text-xs font-medium text-white">{asset.unitName}</Text>
|
|
77
|
+
{asset.price !== undefined && (
|
|
78
|
+
<Text className="text-xs text-zinc-400">${asset.price}</Text>
|
|
79
|
+
)}
|
|
80
|
+
<CopyButton value={asset.id.toString()} size={12} color="#a1a1aa" />
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isFullscreen = size === 'fullscreen';
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<SizeContainer size={size} className={className}>
|
|
89
|
+
<Animated.View
|
|
90
|
+
entering={FadeInDown.duration(400).springify()}
|
|
91
|
+
className={`rounded-2xl ${isFullscreen ? 'border border-zinc-800' : 'bg-zinc-900/80'}`}
|
|
92
|
+
>
|
|
93
|
+
<View className="p-5">
|
|
94
|
+
{/* NFT Image */}
|
|
95
|
+
{isNFT && imageUrl && (
|
|
96
|
+
<View className="mb-4 rounded-2xl overflow-hidden aspect-square">
|
|
97
|
+
<Image source={{ uri: imageUrl }} style={{ width: '100%', height: '100%' }} contentFit="cover" />
|
|
98
|
+
<View className="absolute top-2 left-2 flex-row gap-1">
|
|
99
|
+
<View className="px-2 py-1 rounded-full bg-purple-500/20">
|
|
100
|
+
<Text className="text-xs font-medium text-purple-300">NFT</Text>
|
|
101
|
+
</View>
|
|
102
|
+
</View>
|
|
103
|
+
{asset.verified && (
|
|
104
|
+
<View className="absolute top-2 right-2">
|
|
105
|
+
<View className="px-2 py-1 rounded-full bg-blue-500/20">
|
|
106
|
+
<ShieldIcon size={12} color="#93c5fd" />
|
|
107
|
+
</View>
|
|
108
|
+
</View>
|
|
109
|
+
)}
|
|
110
|
+
</View>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{/* Regular Asset Header */}
|
|
114
|
+
{!isNFT && (
|
|
115
|
+
<View className="flex-row items-center gap-3 mb-4">
|
|
116
|
+
{imageUrl && (
|
|
117
|
+
<View className="w-10 h-10 rounded-lg overflow-hidden bg-zinc-800/50">
|
|
118
|
+
<Image source={{ uri: imageUrl }} style={{ width: 40, height: 40 }} contentFit="cover" />
|
|
119
|
+
</View>
|
|
120
|
+
)}
|
|
121
|
+
<View className="flex-1">
|
|
122
|
+
<View className="flex-row items-center justify-between mb-0.5">
|
|
123
|
+
<Text className="text-sm font-semibold text-white flex-1 mr-2" numberOfLines={1}>
|
|
124
|
+
{asset.name}
|
|
125
|
+
</Text>
|
|
126
|
+
<View className="flex-row items-center gap-1">
|
|
127
|
+
{asset.verified && <ShieldIcon size={16} color="#60a5fa" />}
|
|
128
|
+
{asset.defaultFrozen && <LockIcon size={16} />}
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
<View className="flex-row items-center justify-between">
|
|
132
|
+
<Text className="text-xs font-mono text-zinc-400">
|
|
133
|
+
{asset.unitName} - #{asset.id}
|
|
134
|
+
</Text>
|
|
135
|
+
<CopyButton value={asset.id.toString()} size={12} color="#71717a" />
|
|
136
|
+
</View>
|
|
137
|
+
</View>
|
|
138
|
+
</View>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* NFT Title */}
|
|
142
|
+
{isNFT && (
|
|
143
|
+
<View className="mb-4">
|
|
144
|
+
<Text className="text-lg font-bold text-white mb-1">{asset.name}</Text>
|
|
145
|
+
<View className="flex-row items-center gap-2">
|
|
146
|
+
<Text className="text-xs font-mono text-zinc-400">
|
|
147
|
+
{asset.unitName} - #{asset.id}
|
|
148
|
+
</Text>
|
|
149
|
+
<CopyButton value={asset.id.toString()} size={12} color="#71717a" />
|
|
150
|
+
</View>
|
|
151
|
+
</View>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Stats */}
|
|
155
|
+
<View className="flex-row gap-6 mb-3">
|
|
156
|
+
{asset.price !== undefined && (
|
|
157
|
+
<View>
|
|
158
|
+
<Text className="text-2xl font-bold text-white">${formatNumber(asset.price)}</Text>
|
|
159
|
+
<Text className="text-xs text-zinc-400">Price (USD)</Text>
|
|
160
|
+
</View>
|
|
161
|
+
)}
|
|
162
|
+
<View>
|
|
163
|
+
<Text className="text-base font-semibold text-white">
|
|
164
|
+
{formatAssetAmount(asset.total, asset.decimals)}
|
|
165
|
+
</Text>
|
|
166
|
+
<Text className="text-xs text-zinc-400">Supply</Text>
|
|
167
|
+
</View>
|
|
168
|
+
<View>
|
|
169
|
+
<Text className="text-base font-semibold text-white">{asset.decimals}</Text>
|
|
170
|
+
<Text className="text-xs text-zinc-400">Decimals</Text>
|
|
171
|
+
</View>
|
|
172
|
+
</View>
|
|
173
|
+
|
|
174
|
+
{/* Details */}
|
|
175
|
+
{showDetails && (
|
|
176
|
+
<View className="pt-3 border-t border-zinc-800">
|
|
177
|
+
<Text className="text-sm font-medium text-white mb-2">Asset Details</Text>
|
|
178
|
+
|
|
179
|
+
<View className="flex-row justify-between p-2 rounded-lg bg-zinc-800/50 mb-1">
|
|
180
|
+
<Text className="text-xs text-zinc-300">Default Frozen</Text>
|
|
181
|
+
<View className="flex-row items-center gap-1">
|
|
182
|
+
{asset.defaultFrozen ? (
|
|
183
|
+
<>
|
|
184
|
+
<LockIcon size={12} color="#fb923c" />
|
|
185
|
+
<Text className="text-xs font-medium text-white">Yes</Text>
|
|
186
|
+
</>
|
|
187
|
+
) : (
|
|
188
|
+
<>
|
|
189
|
+
<UnlockIcon size={12} color="#a1a1aa" />
|
|
190
|
+
<Text className="text-xs font-medium text-white">No</Text>
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
193
|
+
</View>
|
|
194
|
+
</View>
|
|
195
|
+
|
|
196
|
+
<View className="flex-row justify-between p-2 rounded-lg bg-zinc-800/50 mb-1">
|
|
197
|
+
<Text className="text-xs text-zinc-300">Creator</Text>
|
|
198
|
+
<Text className="text-xs font-mono text-white">{asset.creator.slice(0, 8)}...</Text>
|
|
199
|
+
</View>
|
|
200
|
+
|
|
201
|
+
{asset.url && (
|
|
202
|
+
<View className="flex-row justify-between p-2 rounded-lg bg-zinc-800/50">
|
|
203
|
+
<Text className="text-xs text-zinc-300">URL</Text>
|
|
204
|
+
<Text className="text-xs font-mono text-white max-w-[120px]" numberOfLines={1}>
|
|
205
|
+
{asset.url}
|
|
206
|
+
</Text>
|
|
207
|
+
</View>
|
|
208
|
+
)}
|
|
209
|
+
</View>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Created Date */}
|
|
213
|
+
<View className="flex-row items-center gap-1.5 mt-3">
|
|
214
|
+
<CalendarIcon />
|
|
215
|
+
<Text className="text-xs text-zinc-600">Created {formatRelativeTime(asset.createdAt)}</Text>
|
|
216
|
+
</View>
|
|
217
|
+
</View>
|
|
218
|
+
</Animated.View>
|
|
219
|
+
</SizeContainer>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
import Svg, { Path } from 'react-native-svg';
|
|
4
|
+
import type { ASA } from '../types/algorand';
|
|
5
|
+
import { SearchSheet } from '../ui/SearchSheet';
|
|
6
|
+
|
|
7
|
+
function ShieldIcon({ size = 12, color = '#60a5fa' }: { size?: number; color?: string }) {
|
|
8
|
+
return (
|
|
9
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
10
|
+
<Path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
|
|
11
|
+
</Svg>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ASASearchProps {
|
|
16
|
+
data: ASA[];
|
|
17
|
+
onSelect?: (asset: ASA) => void;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ASASearch({
|
|
23
|
+
data,
|
|
24
|
+
onSelect,
|
|
25
|
+
placeholder = 'Search assets...',
|
|
26
|
+
className,
|
|
27
|
+
}: ASASearchProps) {
|
|
28
|
+
const [selected, setSelected] = useState<ASA | null>(null);
|
|
29
|
+
|
|
30
|
+
const handleSelect = useCallback(
|
|
31
|
+
(asset: ASA) => {
|
|
32
|
+
setSelected(asset);
|
|
33
|
+
onSelect?.(asset);
|
|
34
|
+
},
|
|
35
|
+
[onSelect],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const renderItem = useCallback(
|
|
39
|
+
(asset: ASA) => (
|
|
40
|
+
<View className="flex-row items-center gap-3">
|
|
41
|
+
<View className="w-8 h-8 rounded-lg bg-zinc-800 items-center justify-center">
|
|
42
|
+
<Text className="text-xs text-zinc-400">{asset.unitName.slice(0, 2)}</Text>
|
|
43
|
+
</View>
|
|
44
|
+
<View className="flex-1">
|
|
45
|
+
<View className="flex-row items-center gap-1">
|
|
46
|
+
<Text className="text-sm font-medium text-white" numberOfLines={1}>
|
|
47
|
+
{asset.name}
|
|
48
|
+
</Text>
|
|
49
|
+
{asset.verified && <ShieldIcon />}
|
|
50
|
+
</View>
|
|
51
|
+
<Text className="text-xs text-zinc-400">
|
|
52
|
+
{asset.unitName} - #{asset.id}
|
|
53
|
+
</Text>
|
|
54
|
+
</View>
|
|
55
|
+
{asset.price !== undefined && (
|
|
56
|
+
<Text className="text-xs text-zinc-300">${asset.price}</Text>
|
|
57
|
+
)}
|
|
58
|
+
</View>
|
|
59
|
+
),
|
|
60
|
+
[],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View className={className}>
|
|
65
|
+
<SearchSheet
|
|
66
|
+
data={data}
|
|
67
|
+
placeholder={placeholder}
|
|
68
|
+
onSelect={handleSelect}
|
|
69
|
+
renderItem={renderItem}
|
|
70
|
+
/>
|
|
71
|
+
{selected && (
|
|
72
|
+
<View className="mt-3 p-3 rounded-lg bg-zinc-800 border border-zinc-700">
|
|
73
|
+
<Text className="text-xs font-medium text-zinc-400 mb-1">Selected Asset</Text>
|
|
74
|
+
{renderItem(selected)}
|
|
75
|
+
</View>
|
|
76
|
+
)}
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Pressable } from 'react-native';
|
|
3
|
+
import Animated, { FadeInDown } from 'react-native-reanimated';
|
|
4
|
+
import type { AlgorandAccount, ComponentSize } from '../types/algorand';
|
|
5
|
+
import { formatAddress, formatCurrency, formatRelativeTime } from '../utils/format';
|
|
6
|
+
import { CopyButton } from '../ui/CopyButton';
|
|
7
|
+
import { SizeContainer } from '../ui/SizeContainer';
|
|
8
|
+
import Svg, { Circle as SvgCircle, Path } from 'react-native-svg';
|
|
9
|
+
|
|
10
|
+
function OnlineIndicator({ isOnline, size = 8 }: { isOnline: boolean; size?: number }) {
|
|
11
|
+
return (
|
|
12
|
+
<Svg width={size} height={size} viewBox="0 0 8 8">
|
|
13
|
+
<SvgCircle cx="4" cy="4" r="4" fill={isOnline ? '#34d399' : '#71717a'} />
|
|
14
|
+
</Svg>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ExternalLinkIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
21
|
+
<Path d="M15 3h6v6" />
|
|
22
|
+
<Path d="M10 14 21 3" />
|
|
23
|
+
<Path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
24
|
+
</Svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AccountProps {
|
|
29
|
+
data: AlgorandAccount;
|
|
30
|
+
showAssets?: boolean;
|
|
31
|
+
showApps?: boolean;
|
|
32
|
+
compact?: boolean;
|
|
33
|
+
size?: ComponentSize;
|
|
34
|
+
className?: string;
|
|
35
|
+
onExternalLink?: (address: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function Account({
|
|
39
|
+
data: account,
|
|
40
|
+
showAssets = false,
|
|
41
|
+
showApps = false,
|
|
42
|
+
compact = false,
|
|
43
|
+
size = 'full',
|
|
44
|
+
className,
|
|
45
|
+
onExternalLink,
|
|
46
|
+
}: AccountProps) {
|
|
47
|
+
if (compact) {
|
|
48
|
+
return (
|
|
49
|
+
<View className={`flex-row items-center gap-2 px-2 py-1 rounded-full bg-zinc-800/60 ${className ?? ''}`}>
|
|
50
|
+
<OnlineIndicator isOnline={account.isOnline} />
|
|
51
|
+
<Text className="font-mono text-xs text-white" numberOfLines={1}>
|
|
52
|
+
{formatAddress(account.address)}
|
|
53
|
+
</Text>
|
|
54
|
+
<CopyButton value={account.address} size={12} color="#a1a1aa" />
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isFullscreen = size === 'fullscreen';
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<SizeContainer size={size} className={className}>
|
|
63
|
+
<Animated.View
|
|
64
|
+
entering={FadeInDown.duration(400).springify()}
|
|
65
|
+
className={`rounded-2xl ${isFullscreen ? 'border border-zinc-800' : 'bg-zinc-900/80'}`}
|
|
66
|
+
>
|
|
67
|
+
<View className="p-5">
|
|
68
|
+
{/* Header */}
|
|
69
|
+
<View className="flex-row items-start justify-between mb-4">
|
|
70
|
+
<View className="flex-1 mr-2">
|
|
71
|
+
<View className="flex-row items-center gap-2 mb-1">
|
|
72
|
+
<OnlineIndicator isOnline={account.isOnline} size={10} />
|
|
73
|
+
<Text className="text-xs font-medium text-zinc-400">
|
|
74
|
+
{account.isOnline ? 'Online' : 'Offline'}
|
|
75
|
+
</Text>
|
|
76
|
+
</View>
|
|
77
|
+
<Text className="font-mono text-sm font-medium text-white" numberOfLines={1}>
|
|
78
|
+
{formatAddress(account.address)}
|
|
79
|
+
</Text>
|
|
80
|
+
</View>
|
|
81
|
+
<View className="flex-row items-center gap-1">
|
|
82
|
+
<CopyButton value={account.address} size={16} color="#a1a1aa" />
|
|
83
|
+
{onExternalLink && (
|
|
84
|
+
<Pressable onPress={() => onExternalLink(account.address)} className="p-1.5 rounded-lg" hitSlop={8}>
|
|
85
|
+
<ExternalLinkIcon size={16} color="#a1a1aa" />
|
|
86
|
+
</Pressable>
|
|
87
|
+
)}
|
|
88
|
+
</View>
|
|
89
|
+
</View>
|
|
90
|
+
|
|
91
|
+
{/* Balance */}
|
|
92
|
+
<View className="mb-4">
|
|
93
|
+
<Text className="text-2xl font-bold text-white">
|
|
94
|
+
{formatCurrency(account.balance / 1_000_000)}
|
|
95
|
+
</Text>
|
|
96
|
+
<Text className="text-xs text-zinc-400">
|
|
97
|
+
Balance - Round #{account.round.toLocaleString()}
|
|
98
|
+
</Text>
|
|
99
|
+
</View>
|
|
100
|
+
|
|
101
|
+
{/* Stats */}
|
|
102
|
+
<View className="flex-row gap-6 mb-3">
|
|
103
|
+
<View>
|
|
104
|
+
<Text className="text-base font-semibold text-white">
|
|
105
|
+
{account.assets.length}
|
|
106
|
+
</Text>
|
|
107
|
+
<Text className="text-xs text-zinc-400">Assets</Text>
|
|
108
|
+
</View>
|
|
109
|
+
<View>
|
|
110
|
+
<Text className="text-base font-semibold text-white">
|
|
111
|
+
{account.apps.length}
|
|
112
|
+
</Text>
|
|
113
|
+
<Text className="text-xs text-zinc-400">Apps</Text>
|
|
114
|
+
</View>
|
|
115
|
+
</View>
|
|
116
|
+
|
|
117
|
+
{/* Created Date */}
|
|
118
|
+
<Text className="text-xs text-zinc-600 mb-3">
|
|
119
|
+
Created {formatRelativeTime(account.createdAt)}
|
|
120
|
+
</Text>
|
|
121
|
+
|
|
122
|
+
{/* Assets List */}
|
|
123
|
+
{showAssets && account.assets.length > 0 && (
|
|
124
|
+
<View className="pt-3 border-t border-zinc-800">
|
|
125
|
+
<Text className="text-sm font-medium text-white mb-2">
|
|
126
|
+
Assets ({account.assets.length})
|
|
127
|
+
</Text>
|
|
128
|
+
{account.assets.slice(0, 3).map((asset) => (
|
|
129
|
+
<View
|
|
130
|
+
key={asset.id}
|
|
131
|
+
className="flex-row items-center justify-between p-2 rounded-lg bg-zinc-800/50 mb-1"
|
|
132
|
+
>
|
|
133
|
+
<Text className="text-xs font-medium text-white flex-1 mr-2" numberOfLines={1}>
|
|
134
|
+
{asset.name}
|
|
135
|
+
</Text>
|
|
136
|
+
<Text className="text-xs font-mono text-zinc-400">{asset.unitName}</Text>
|
|
137
|
+
</View>
|
|
138
|
+
))}
|
|
139
|
+
{account.assets.length > 3 && (
|
|
140
|
+
<Text className="text-xs text-zinc-500 text-center py-1">
|
|
141
|
+
+{account.assets.length - 3} more
|
|
142
|
+
</Text>
|
|
143
|
+
)}
|
|
144
|
+
</View>
|
|
145
|
+
)}
|
|
146
|
+
</View>
|
|
147
|
+
</Animated.View>
|
|
148
|
+
</SizeContainer>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
import Svg, { Circle as SvgCircle } from 'react-native-svg';
|
|
4
|
+
import type { AlgorandAccount } from '../types/algorand';
|
|
5
|
+
import { formatAddress } from '../utils/format';
|
|
6
|
+
import { SearchSheet } from '../ui/SearchSheet';
|
|
7
|
+
|
|
8
|
+
function OnlineIndicator({ isOnline }: { isOnline: boolean }) {
|
|
9
|
+
return (
|
|
10
|
+
<Svg width={8} height={8} viewBox="0 0 8 8">
|
|
11
|
+
<SvgCircle cx="4" cy="4" r="4" fill={isOnline ? '#34d399' : '#71717a'} />
|
|
12
|
+
</Svg>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AccountSearchProps {
|
|
17
|
+
data: AlgorandAccount[];
|
|
18
|
+
onSelect?: (account: AlgorandAccount) => void;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AccountSearch({
|
|
24
|
+
data,
|
|
25
|
+
onSelect,
|
|
26
|
+
placeholder = 'Search accounts...',
|
|
27
|
+
className,
|
|
28
|
+
}: AccountSearchProps) {
|
|
29
|
+
const [selected, setSelected] = useState<AlgorandAccount | null>(null);
|
|
30
|
+
|
|
31
|
+
const handleSelect = useCallback(
|
|
32
|
+
(account: AlgorandAccount) => {
|
|
33
|
+
setSelected(account);
|
|
34
|
+
onSelect?.(account);
|
|
35
|
+
},
|
|
36
|
+
[onSelect],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const renderItem = useCallback(
|
|
40
|
+
(account: AlgorandAccount) => (
|
|
41
|
+
<View className="flex-row items-center gap-3">
|
|
42
|
+
<View className="w-8 h-8 rounded-full bg-zinc-800 items-center justify-center">
|
|
43
|
+
<Text className="text-xs text-zinc-400">AC</Text>
|
|
44
|
+
</View>
|
|
45
|
+
<View className="flex-1">
|
|
46
|
+
<Text className="text-sm font-mono font-medium text-white" numberOfLines={1}>
|
|
47
|
+
{formatAddress(account.address)}
|
|
48
|
+
</Text>
|
|
49
|
+
<Text className="text-xs text-zinc-400">
|
|
50
|
+
{account.balance / 1_000_000} ALGO - {account.assets.length} assets
|
|
51
|
+
</Text>
|
|
52
|
+
</View>
|
|
53
|
+
<OnlineIndicator isOnline={account.isOnline} />
|
|
54
|
+
</View>
|
|
55
|
+
),
|
|
56
|
+
[],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<View className={className}>
|
|
61
|
+
<SearchSheet
|
|
62
|
+
data={data}
|
|
63
|
+
placeholder={placeholder}
|
|
64
|
+
onSelect={handleSelect}
|
|
65
|
+
renderItem={renderItem}
|
|
66
|
+
/>
|
|
67
|
+
{selected && (
|
|
68
|
+
<View className="mt-3 p-3 rounded-lg bg-zinc-800 border border-zinc-700">
|
|
69
|
+
<Text className="text-xs font-medium text-zinc-400 mb-1">Selected Account</Text>
|
|
70
|
+
{renderItem(selected)}
|
|
71
|
+
</View>
|
|
72
|
+
)}
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|