@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.
Files changed (41) hide show
  1. package/package.json +46 -0
  2. package/src/components/ASA.tsx +221 -0
  3. package/src/components/ASASearch.tsx +79 -0
  4. package/src/components/Account.tsx +150 -0
  5. package/src/components/AccountSearch.tsx +75 -0
  6. package/src/components/AuctionListing.tsx +220 -0
  7. package/src/components/NFDProfile.tsx +173 -0
  8. package/src/components/NFDSearch.tsx +81 -0
  9. package/src/components/NFTListing.tsx +181 -0
  10. package/src/components/NFTSearch.tsx +68 -0
  11. package/src/components/Poll.tsx +234 -0
  12. package/src/components/PollSearch.tsx +79 -0
  13. package/src/components/RaffleListing.tsx +204 -0
  14. package/src/components/SelfFetching.tsx +358 -0
  15. package/src/components/TradeOffer.tsx +245 -0
  16. package/src/components/TradeSearch.tsx +103 -0
  17. package/src/components/TransactionDetails.tsx +242 -0
  18. package/src/components/TransactionSearch.tsx +102 -0
  19. package/src/components/index.ts +30 -0
  20. package/src/hooks/index.ts +12 -0
  21. package/src/hooks/useAlgomdData.ts +445 -0
  22. package/src/hooks/useAlgorandClient.ts +39 -0
  23. package/src/index.ts +83 -0
  24. package/src/provider/AlgomdProvider.tsx +45 -0
  25. package/src/provider/context.ts +18 -0
  26. package/src/provider/imageResolver.ts +12 -0
  27. package/src/provider/index.ts +5 -0
  28. package/src/provider/networks.ts +23 -0
  29. package/src/provider/types.ts +17 -0
  30. package/src/types/algorand.ts +205 -0
  31. package/src/types/index.ts +1 -0
  32. package/src/ui/CopyButton.tsx +40 -0
  33. package/src/ui/DataStates.tsx +41 -0
  34. package/src/ui/ProgressBar.tsx +42 -0
  35. package/src/ui/SearchSheet.tsx +134 -0
  36. package/src/ui/SizeContainer.tsx +25 -0
  37. package/src/ui/StatusBadge.tsx +51 -0
  38. package/src/ui/index.ts +6 -0
  39. package/src/utils/format.ts +70 -0
  40. package/src/utils/index.ts +2 -0
  41. package/src/utils/search.ts +65 -0
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Self-fetching wrappers for algomd-rn display components.
3
+ *
4
+ * Each wrapper adds an optional self-fetch prop (appId, address, txId, etc.)
5
+ * alongside the existing `data` prop. If `data` is provided, it renders
6
+ * immediately. Otherwise, it fetches from algod via the hooks.
7
+ */
8
+
9
+ import React from 'react'
10
+ import type { ReactNode } from 'react'
11
+ import type { ComponentSize } from '../types/algorand'
12
+ import { LoadingSkeleton, ErrorState } from '../ui/DataStates'
13
+
14
+ // Display components (internal names)
15
+ import { Account as AccountDisplay } from './Account'
16
+ import { ASAComponent } from './ASA'
17
+ import { NFTListingComponent } from './NFTListing'
18
+ import { NFDProfileComponent } from './NFDProfile'
19
+ import { TransactionDetailsComponent } from './TransactionDetails'
20
+ import { PollComponent } from './Poll'
21
+ import { RaffleListingComponent } from './RaffleListing'
22
+ import { AuctionListingComponent } from './AuctionListing'
23
+ import { TradeOfferComponent } from './TradeOffer'
24
+
25
+ // Hooks
26
+ import {
27
+ useAccountData,
28
+ useASAData,
29
+ useTransactionData,
30
+ useNFDProfileData,
31
+ usePollData,
32
+ useRaffleData,
33
+ useAuctionData,
34
+ useNFTListingData,
35
+ useTradeOfferData,
36
+ } from '../hooks/useAlgomdData'
37
+
38
+ // Types
39
+ import type {
40
+ AlgorandAccount,
41
+ ASA as ASAType,
42
+ NFTListing as NFTListingType,
43
+ NFDProfile as NFDProfileType,
44
+ TransactionDetails as TransactionDetailsType,
45
+ Poll as PollType,
46
+ RaffleListing as RaffleListingType,
47
+ AuctionListing as AuctionListingType,
48
+ TradeOffer as TradeOfferType,
49
+ } from '../types/algorand'
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Shared fetcher wrapper
53
+ // ---------------------------------------------------------------------------
54
+
55
+ function FetcherWrapper({
56
+ name,
57
+ isLoading,
58
+ error,
59
+ data,
60
+ loadingFallback,
61
+ errorFallback,
62
+ children,
63
+ }: {
64
+ name: string
65
+ isLoading: boolean
66
+ error: Error | null
67
+ data: unknown
68
+ loadingFallback?: ReactNode
69
+ errorFallback?: ReactNode
70
+ children: ReactNode
71
+ }) {
72
+ if (isLoading) return <>{loadingFallback ?? <LoadingSkeleton name={name} />}</>
73
+ if (error || !data) return <>{errorFallback ?? <ErrorState name={name} message={error?.message ?? 'Not found'} />}</>
74
+ return <>{children}</>
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Poll
79
+ // ---------------------------------------------------------------------------
80
+
81
+ type PollSelfFetchProps = {
82
+ data?: PollType
83
+ appId?: number
84
+ showVoteButton?: boolean
85
+ compact?: boolean
86
+ size?: ComponentSize
87
+ className?: string
88
+ onVote?: (pollId: string, optionId: string) => void
89
+ loadingFallback?: ReactNode
90
+ errorFallback?: ReactNode
91
+ }
92
+
93
+ function PollFetcher({ appId, ...rest }: Omit<PollSelfFetchProps, 'data'> & { appId: number }) {
94
+ const { data, isLoading, error } = usePollData(appId)
95
+ return (
96
+ <FetcherWrapper name="Poll" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
97
+ {data && <PollComponent data={data} showVoteButton={rest.showVoteButton} compact={rest.compact} size={rest.size} className={rest.className} onVote={rest.onVote} />}
98
+ </FetcherWrapper>
99
+ )
100
+ }
101
+
102
+ export function Poll(props: PollSelfFetchProps) {
103
+ if (props.data) return <PollComponent data={props.data} showVoteButton={props.showVoteButton} compact={props.compact} size={props.size} className={props.className} onVote={props.onVote} />
104
+ if (props.appId != null) return <PollFetcher appId={props.appId} showVoteButton={props.showVoteButton} compact={props.compact} size={props.size} className={props.className} onVote={props.onVote} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
105
+ return <ErrorState name="Poll" message="Either data or appId is required" />
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // RaffleListing
110
+ // ---------------------------------------------------------------------------
111
+
112
+ type RaffleListingSelfFetchProps = {
113
+ data?: RaffleListingType
114
+ appId?: number
115
+ showEntryButton?: boolean
116
+ size?: ComponentSize
117
+ className?: string
118
+ imageUrl?: string
119
+ onEnter?: (raffle: RaffleListingType) => void
120
+ loadingFallback?: ReactNode
121
+ errorFallback?: ReactNode
122
+ }
123
+
124
+ function RaffleListingFetcher({ appId, ...rest }: Omit<RaffleListingSelfFetchProps, 'data'> & { appId: number }) {
125
+ const { data, isLoading, error } = useRaffleData(appId)
126
+ return (
127
+ <FetcherWrapper name="Raffle" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
128
+ {data && <RaffleListingComponent data={data} showEntryButton={rest.showEntryButton} size={rest.size} className={rest.className} imageUrl={rest.imageUrl} onEnter={rest.onEnter} />}
129
+ </FetcherWrapper>
130
+ )
131
+ }
132
+
133
+ export function RaffleListing(props: RaffleListingSelfFetchProps) {
134
+ if (props.data) return <RaffleListingComponent data={props.data} showEntryButton={props.showEntryButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onEnter={props.onEnter} />
135
+ if (props.appId != null) return <RaffleListingFetcher appId={props.appId} showEntryButton={props.showEntryButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onEnter={props.onEnter} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
136
+ return <ErrorState name="Raffle" message="Either data or appId is required" />
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // AuctionListing
141
+ // ---------------------------------------------------------------------------
142
+
143
+ type AuctionListingSelfFetchProps = {
144
+ data?: AuctionListingType
145
+ appId?: number
146
+ showBidButton?: boolean
147
+ size?: ComponentSize
148
+ className?: string
149
+ imageUrl?: string
150
+ onBid?: (auction: AuctionListingType) => void
151
+ loadingFallback?: ReactNode
152
+ errorFallback?: ReactNode
153
+ }
154
+
155
+ function AuctionListingFetcher({ appId, ...rest }: Omit<AuctionListingSelfFetchProps, 'data'> & { appId: number }) {
156
+ const { data, isLoading, error } = useAuctionData(appId)
157
+ return (
158
+ <FetcherWrapper name="Auction" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
159
+ {data && <AuctionListingComponent data={data} showBidButton={rest.showBidButton} size={rest.size} className={rest.className} imageUrl={rest.imageUrl} onBid={rest.onBid} />}
160
+ </FetcherWrapper>
161
+ )
162
+ }
163
+
164
+ export function AuctionListing(props: AuctionListingSelfFetchProps) {
165
+ if (props.data) return <AuctionListingComponent data={props.data} showBidButton={props.showBidButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onBid={props.onBid} />
166
+ if (props.appId != null) return <AuctionListingFetcher appId={props.appId} showBidButton={props.showBidButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onBid={props.onBid} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
167
+ return <ErrorState name="Auction" message="Either data or appId is required" />
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // NFTListing
172
+ // ---------------------------------------------------------------------------
173
+
174
+ type NFTListingSelfFetchProps = {
175
+ data?: NFTListingType
176
+ appId?: number
177
+ showPurchaseButton?: boolean
178
+ size?: ComponentSize
179
+ className?: string
180
+ imageUrl?: string
181
+ onPurchase?: (listing: NFTListingType) => void
182
+ onFavorite?: (listing: NFTListingType) => void
183
+ loadingFallback?: ReactNode
184
+ errorFallback?: ReactNode
185
+ }
186
+
187
+ function NFTListingFetcher({ appId, ...rest }: Omit<NFTListingSelfFetchProps, 'data'> & { appId: number }) {
188
+ const { data, isLoading, error } = useNFTListingData(appId)
189
+ return (
190
+ <FetcherWrapper name="NFT Listing" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
191
+ {data && <NFTListingComponent data={data} showPurchaseButton={rest.showPurchaseButton} size={rest.size} className={rest.className} imageUrl={rest.imageUrl} onPurchase={rest.onPurchase} onFavorite={rest.onFavorite} />}
192
+ </FetcherWrapper>
193
+ )
194
+ }
195
+
196
+ export function NFTListing(props: NFTListingSelfFetchProps) {
197
+ if (props.data) return <NFTListingComponent data={props.data} showPurchaseButton={props.showPurchaseButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onPurchase={props.onPurchase} onFavorite={props.onFavorite} />
198
+ if (props.appId != null) return <NFTListingFetcher appId={props.appId} showPurchaseButton={props.showPurchaseButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onPurchase={props.onPurchase} onFavorite={props.onFavorite} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
199
+ return <ErrorState name="NFT Listing" message="Either data or appId is required" />
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // TradeOffer
204
+ // ---------------------------------------------------------------------------
205
+
206
+ type TradeOfferSelfFetchProps = {
207
+ data?: TradeOfferType
208
+ appId?: number
209
+ offerId?: number
210
+ showActions?: boolean
211
+ size?: ComponentSize
212
+ className?: string
213
+ currentUserAddress?: string
214
+ onAccept?: (offer: TradeOfferType) => void
215
+ onReject?: (offer: TradeOfferType) => void
216
+ loadingFallback?: ReactNode
217
+ errorFallback?: ReactNode
218
+ }
219
+
220
+ function TradeOfferFetcher({ appId, offerId, ...rest }: Omit<TradeOfferSelfFetchProps, 'data'> & { appId: number; offerId: number }) {
221
+ const { data, isLoading, error } = useTradeOfferData(appId, offerId)
222
+ return (
223
+ <FetcherWrapper name="Trade Offer" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
224
+ {data && <TradeOfferComponent data={data} showActions={rest.showActions} size={rest.size} className={rest.className} currentUserAddress={rest.currentUserAddress} onAccept={rest.onAccept} onReject={rest.onReject} />}
225
+ </FetcherWrapper>
226
+ )
227
+ }
228
+
229
+ export function TradeOffer(props: TradeOfferSelfFetchProps) {
230
+ if (props.data) return <TradeOfferComponent data={props.data} showActions={props.showActions} size={props.size} className={props.className} currentUserAddress={props.currentUserAddress} onAccept={props.onAccept} onReject={props.onReject} />
231
+ if (props.appId != null && props.offerId != null) return <TradeOfferFetcher appId={props.appId} offerId={props.offerId} showActions={props.showActions} size={props.size} className={props.className} currentUserAddress={props.currentUserAddress} onAccept={props.onAccept} onReject={props.onReject} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
232
+ return <ErrorState name="Trade Offer" message="Either data or (appId + offerId) is required" />
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Account
237
+ // ---------------------------------------------------------------------------
238
+
239
+ type AccountSelfFetchProps = {
240
+ data?: AlgorandAccount
241
+ address?: string
242
+ showAssets?: boolean
243
+ showApps?: boolean
244
+ compact?: boolean
245
+ size?: ComponentSize
246
+ className?: string
247
+ onExternalLink?: (address: string) => void
248
+ loadingFallback?: ReactNode
249
+ errorFallback?: ReactNode
250
+ }
251
+
252
+ function AccountFetcher({ address, ...rest }: Omit<AccountSelfFetchProps, 'data'> & { address: string }) {
253
+ const { data, isLoading, error } = useAccountData(address)
254
+ return (
255
+ <FetcherWrapper name="Account" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
256
+ {data && <AccountDisplay data={data} showAssets={rest.showAssets} showApps={rest.showApps} compact={rest.compact} size={rest.size} className={rest.className} onExternalLink={rest.onExternalLink} />}
257
+ </FetcherWrapper>
258
+ )
259
+ }
260
+
261
+ export function Account(props: AccountSelfFetchProps) {
262
+ if (props.data) return <AccountDisplay data={props.data} showAssets={props.showAssets} showApps={props.showApps} compact={props.compact} size={props.size} className={props.className} onExternalLink={props.onExternalLink} />
263
+ if (props.address) return <AccountFetcher address={props.address} showAssets={props.showAssets} showApps={props.showApps} compact={props.compact} size={props.size} className={props.className} onExternalLink={props.onExternalLink} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
264
+ return <ErrorState name="Account" message="Either data or address is required" />
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // ASA
269
+ // ---------------------------------------------------------------------------
270
+
271
+ type ASASelfFetchProps = {
272
+ data?: ASAType
273
+ assetId?: number
274
+ showDetails?: boolean
275
+ compact?: boolean
276
+ size?: ComponentSize
277
+ className?: string
278
+ imageUrl?: string
279
+ loadingFallback?: ReactNode
280
+ errorFallback?: ReactNode
281
+ }
282
+
283
+ function ASAFetcher({ assetId, ...rest }: Omit<ASASelfFetchProps, 'data'> & { assetId: number }) {
284
+ const { data, isLoading, error } = useASAData(assetId)
285
+ return (
286
+ <FetcherWrapper name="ASA" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
287
+ {data && <ASAComponent data={data} showDetails={rest.showDetails} compact={rest.compact} size={rest.size} className={rest.className} imageUrl={rest.imageUrl} />}
288
+ </FetcherWrapper>
289
+ )
290
+ }
291
+
292
+ export function ASA(props: ASASelfFetchProps) {
293
+ if (props.data) return <ASAComponent data={props.data} showDetails={props.showDetails} compact={props.compact} size={props.size} className={props.className} imageUrl={props.imageUrl} />
294
+ if (props.assetId != null) return <ASAFetcher assetId={props.assetId} showDetails={props.showDetails} compact={props.compact} size={props.size} className={props.className} imageUrl={props.imageUrl} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
295
+ return <ErrorState name="ASA" message="Either data or assetId is required" />
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // TransactionDetails
300
+ // ---------------------------------------------------------------------------
301
+
302
+ type TransactionDetailsSelfFetchProps = {
303
+ data?: TransactionDetailsType
304
+ txId?: string
305
+ showContext?: boolean
306
+ showFee?: boolean
307
+ compact?: boolean
308
+ size?: ComponentSize
309
+ className?: string
310
+ loadingFallback?: ReactNode
311
+ errorFallback?: ReactNode
312
+ }
313
+
314
+ function TransactionDetailsFetcher({ txId, ...rest }: Omit<TransactionDetailsSelfFetchProps, 'data'> & { txId: string }) {
315
+ const { data, isLoading, error } = useTransactionData(txId)
316
+ return (
317
+ <FetcherWrapper name="Transaction" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
318
+ {data && <TransactionDetailsComponent data={data} showContext={rest.showContext} showFee={rest.showFee} compact={rest.compact} size={rest.size} className={rest.className} />}
319
+ </FetcherWrapper>
320
+ )
321
+ }
322
+
323
+ export function TransactionDetails(props: TransactionDetailsSelfFetchProps) {
324
+ if (props.data) return <TransactionDetailsComponent data={props.data} showContext={props.showContext} showFee={props.showFee} compact={props.compact} size={props.size} className={props.className} />
325
+ if (props.txId) return <TransactionDetailsFetcher txId={props.txId} showContext={props.showContext} showFee={props.showFee} compact={props.compact} size={props.size} className={props.className} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
326
+ return <ErrorState name="Transaction" message="Either data or txId is required" />
327
+ }
328
+
329
+ // ---------------------------------------------------------------------------
330
+ // NFDProfile
331
+ // ---------------------------------------------------------------------------
332
+
333
+ type NFDProfileSelfFetchProps = {
334
+ data?: NFDProfileType
335
+ name?: string
336
+ showBio?: boolean
337
+ showProperties?: boolean
338
+ compact?: boolean
339
+ size?: ComponentSize
340
+ className?: string
341
+ loadingFallback?: ReactNode
342
+ errorFallback?: ReactNode
343
+ }
344
+
345
+ function NFDProfileFetcher({ name, ...rest }: Omit<NFDProfileSelfFetchProps, 'data'> & { name: string }) {
346
+ const { data, isLoading, error } = useNFDProfileData(name)
347
+ return (
348
+ <FetcherWrapper name="NFD Profile" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
349
+ {data && <NFDProfileComponent data={data} showBio={rest.showBio} showProperties={rest.showProperties} compact={rest.compact} size={rest.size} className={rest.className} />}
350
+ </FetcherWrapper>
351
+ )
352
+ }
353
+
354
+ export function NFDProfile(props: NFDProfileSelfFetchProps) {
355
+ if (props.data) return <NFDProfileComponent data={props.data} showBio={props.showBio} showProperties={props.showProperties} compact={props.compact} size={props.size} className={props.className} />
356
+ if (props.name) return <NFDProfileFetcher name={props.name} showBio={props.showBio} showProperties={props.showProperties} compact={props.compact} size={props.size} className={props.className} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
357
+ return <ErrorState name="NFD Profile" message="Either data or name is required" />
358
+ }
@@ -0,0 +1,245 @@
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 { TradeOffer as TradeOfferType, ASA, AlgorandAccount, ComponentSize } from '../types/algorand';
7
+ import { formatAddress, formatRelativeTime } from '../utils/format';
8
+ import { CopyButton } from '../ui/CopyButton';
9
+ import { SizeContainer } from '../ui/SizeContainer';
10
+ import { StatusBadge } from '../ui/StatusBadge';
11
+
12
+ function ArrowRightLeftIcon({ size = 20, color = '#a1a1aa' }: { size?: number; color?: string }) {
13
+ return (
14
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
15
+ <Path d="m8 3 4 8 5-5 5 15H2L8 3z" />
16
+ </Svg>
17
+ );
18
+ }
19
+
20
+ function SwapIcon({ size = 20, color = '#a1a1aa' }: { size?: number; color?: string }) {
21
+ return (
22
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
23
+ <Path d="M16 3l4 4-4 4" />
24
+ <Path d="M20 7H4" />
25
+ <Path d="M8 21l-4-4 4-4" />
26
+ <Path d="M4 17h16" />
27
+ </Svg>
28
+ );
29
+ }
30
+
31
+ function CheckIcon({ size = 16, color = '#fff' }: { size?: number; color?: string }) {
32
+ return (
33
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
34
+ <Path d="M20 6 9 17l-5-5" />
35
+ </Svg>
36
+ );
37
+ }
38
+
39
+ function XIcon({ size = 16, color = '#fff' }: { size?: number; color?: string }) {
40
+ return (
41
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
42
+ <Path d="M18 6 6 18" />
43
+ <Path d="m6 6 12 12" />
44
+ </Svg>
45
+ );
46
+ }
47
+
48
+ function ClockIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
49
+ return (
50
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
51
+ <Path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Z" />
52
+ <Path d="M12 6v6l4 2" />
53
+ </Svg>
54
+ );
55
+ }
56
+
57
+ function MessageIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
58
+ return (
59
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
60
+ <Path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" />
61
+ </Svg>
62
+ );
63
+ }
64
+
65
+ // Helper to determine if something is an ASA
66
+ function isASA(item: ASA | AlgorandAccount): item is ASA {
67
+ return 'unitName' in item;
68
+ }
69
+
70
+ function AssetItem({ asset, imageUrl }: { asset: ASA | AlgorandAccount; imageUrl?: string }) {
71
+ if (isASA(asset)) {
72
+ const isNFT = asset.total === 1 && asset.decimals === 0;
73
+ return (
74
+ <View className="flex-row items-center gap-3 p-3 rounded-lg bg-zinc-800/50 mb-2">
75
+ {imageUrl && (
76
+ <View className={`w-10 h-10 overflow-hidden bg-zinc-700 ${isNFT ? 'rounded-lg' : 'rounded-full'}`}>
77
+ <Image source={{ uri: imageUrl }} style={{ width: 40, height: 40 }} contentFit="cover" />
78
+ </View>
79
+ )}
80
+ <View className="flex-1">
81
+ <Text className="text-sm font-medium text-white" numberOfLines={1}>{asset.name}</Text>
82
+ <Text className="text-xs text-zinc-400">{asset.unitName} - #{asset.id}</Text>
83
+ </View>
84
+ {asset.price !== undefined && (
85
+ <Text className="text-xs font-medium text-green-400">${asset.price}</Text>
86
+ )}
87
+ </View>
88
+ );
89
+ }
90
+
91
+ // AlgorandAccount
92
+ return (
93
+ <View className="flex-row items-center gap-3 p-3 rounded-lg bg-zinc-800/50 mb-2">
94
+ <View className="w-10 h-10 rounded-full bg-zinc-700 items-center justify-center">
95
+ <Text className="text-xs text-zinc-400">ACC</Text>
96
+ </View>
97
+ <View className="flex-1">
98
+ <Text className="text-sm font-mono text-white" numberOfLines={1}>
99
+ {formatAddress(asset.address)}
100
+ </Text>
101
+ <Text className="text-xs text-zinc-400">ALGO Account</Text>
102
+ </View>
103
+ </View>
104
+ );
105
+ }
106
+
107
+ function AssetGroup({ title, assets }: { title: string; assets: (ASA | AlgorandAccount)[] }) {
108
+ if (assets.length === 0) {
109
+ return (
110
+ <View className="p-3 rounded-lg border-2 border-dashed border-zinc-700 items-center">
111
+ <Text className="text-sm text-zinc-400">No assets</Text>
112
+ </View>
113
+ );
114
+ }
115
+
116
+ return (
117
+ <View>
118
+ <Text className="text-sm font-medium text-zinc-300 mb-2">{title}</Text>
119
+ {assets.map((asset, index) => (
120
+ <AssetItem key={`${isASA(asset) ? asset.id : asset.address}-${index}`} asset={asset} />
121
+ ))}
122
+ </View>
123
+ );
124
+ }
125
+
126
+ interface TradeOfferProps {
127
+ data: TradeOfferType;
128
+ showActions?: boolean;
129
+ size?: ComponentSize;
130
+ className?: string;
131
+ currentUserAddress?: string;
132
+ onAccept?: (offer: TradeOfferType) => void;
133
+ onReject?: (offer: TradeOfferType) => void;
134
+ }
135
+
136
+ export function TradeOfferComponent({
137
+ data: offer,
138
+ showActions = true,
139
+ size = 'full',
140
+ className,
141
+ currentUserAddress,
142
+ onAccept,
143
+ onReject,
144
+ }: TradeOfferProps) {
145
+ const isExpired = new Date() > offer.expiresAt;
146
+ const isRecipient = currentUserAddress ? offer.recipients.includes(currentUserAddress) : false;
147
+ const canRespond = isRecipient && offer.status === 'pending' && !isExpired;
148
+
149
+ const statusVariant =
150
+ offer.status === 'pending'
151
+ ? 'info'
152
+ : offer.status === 'accepted'
153
+ ? 'success'
154
+ : offer.status === 'rejected'
155
+ ? 'error'
156
+ : 'neutral';
157
+
158
+ const isFullscreen = size === 'fullscreen';
159
+
160
+ return (
161
+ <SizeContainer size={size} className={className}>
162
+ <Animated.View
163
+ entering={FadeInDown.duration(400).springify()}
164
+ className={`rounded-2xl ${isFullscreen ? 'border border-zinc-800' : 'bg-zinc-900/80'}`}
165
+ >
166
+ {/* Header */}
167
+ <View className="px-5 py-3 border-b border-zinc-800">
168
+ <View className="flex-row items-center justify-between">
169
+ <View className="flex-row items-center gap-2">
170
+ <Text className="text-base font-semibold text-white">
171
+ {isRecipient ? 'Incoming Trade' : 'Trade Offer'}
172
+ </Text>
173
+ <StatusBadge
174
+ label={offer.status.charAt(0).toUpperCase() + offer.status.slice(1)}
175
+ variant={statusVariant}
176
+ />
177
+ {isExpired && <StatusBadge label="Expired" variant="error" />}
178
+ </View>
179
+ <View className="flex-row items-center gap-1">
180
+ <ClockIcon size={14} color="#a1a1aa" />
181
+ <Text className="text-xs font-mono text-zinc-300">
182
+ {formatRelativeTime(offer.expiresAt)}
183
+ </Text>
184
+ </View>
185
+ </View>
186
+ <View className="flex-row items-center gap-1 mt-1">
187
+ <Text className="text-xs font-mono text-zinc-400">
188
+ From {formatAddress(offer.creator)}
189
+ </Text>
190
+ <CopyButton value={offer.creator} size={12} color="#71717a" />
191
+ </View>
192
+ </View>
193
+
194
+ {/* Trade Content */}
195
+ <View className="p-5">
196
+ {/* Offering */}
197
+ <AssetGroup title="Offering" assets={offer.offering} />
198
+
199
+ {/* Swap Icon */}
200
+ <View className="items-center py-3">
201
+ <View className="p-2 rounded-full bg-zinc-800/60">
202
+ <SwapIcon size={20} color="#a1a1aa" />
203
+ </View>
204
+ </View>
205
+
206
+ {/* Requesting */}
207
+ <AssetGroup title="Requesting" assets={offer.requesting} />
208
+ </View>
209
+
210
+ {/* Message */}
211
+ {offer.message && (
212
+ <View className="px-5 pb-3 border-t border-zinc-800 pt-3">
213
+ <View className="bg-blue-500/5 rounded-xl px-3 py-2.5">
214
+ <View className="flex-row items-start gap-2">
215
+ <MessageIcon size={14} color="#60a5fa" />
216
+ <Text className="text-sm text-blue-200 flex-1 leading-5">{offer.message}</Text>
217
+ </View>
218
+ </View>
219
+ </View>
220
+ )}
221
+
222
+ {/* Action Buttons */}
223
+ {showActions && canRespond && (
224
+ <View className="px-5 pb-5 flex-row gap-3">
225
+ <Pressable
226
+ onPress={() => onReject?.(offer)}
227
+ className="flex-1 flex-row items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-red-500/20"
228
+ >
229
+ <XIcon size={16} color="#fca5a5" />
230
+ <Text className="text-sm font-medium text-red-300">Reject</Text>
231
+ </Pressable>
232
+
233
+ <Pressable
234
+ onPress={() => onAccept?.(offer)}
235
+ className="flex-1 flex-row items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-green-600"
236
+ >
237
+ <CheckIcon size={16} color="#fff" />
238
+ <Text className="text-sm font-medium text-white">Accept</Text>
239
+ </Pressable>
240
+ </View>
241
+ )}
242
+ </Animated.View>
243
+ </SizeContainer>
244
+ );
245
+ }