Skip to content

Commit ca63edf

Browse files
authored
Merge pull request #190 from dapplets/173-connect-backend-to-the-payments-and-usage-page
feat: Add Payments and Usage page (issue#172, issue#173)
2 parents 7e94e6e + 029f594 commit ca63edf

File tree

12 files changed

+441
-21
lines changed

12 files changed

+441
-21
lines changed

apps/xen-tg-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"preview": "vite preview"
1212
},
1313
"dependencies": {
14+
"@radix-ui/react-accordion": "^1.2.8",
1415
"@radix-ui/react-switch": "^1.1.4",
1516
"@tailwindcss/vite": "^4.1.4",
1617
"@tanstack/react-query": "^5.59.15",

apps/xen-tg-app/src/App.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@ import { Link } from 'react-router'
22
import SettingsIcon from './assets/settings'
33
import XEN_IMAGE from './assets/xen-anime-style-portrait.png'
44
import Capabilities from './components/Capabilities'
5+
import HistoryMainPageModule from './components/HistoryMainPageModule'
56
import Layout from './components/Layout'
67
import Wallet from './components/Wallet'
78

8-
function App() {
9-
return (
10-
<Layout>
11-
<Link
12-
to="/settings"
13-
className="absolute top-5 right-4 rounded-full border border-[#f8f9ff19] bg-(--color-light-white-bg) p-2 backdrop-blur-3xl backdrop-opacity-80"
14-
>
15-
<SettingsIcon />
16-
</Link>
17-
<div className="z-1 m-2.5 flex w-[210px] justify-center overflow-hidden rounded-full select-none">
18-
<img src={XEN_IMAGE} alt="xen-photo" className="h-full w-full" />
19-
</div>
20-
<Wallet />
21-
<Capabilities />
22-
</Layout>
23-
)
24-
}
9+
const App = () => (
10+
<Layout>
11+
<Link
12+
to="/settings"
13+
className="absolute top-5 right-4 rounded-full border border-[#f8f9ff19] bg-(--color-light-white-bg) p-2 backdrop-blur-3xl backdrop-opacity-80"
14+
>
15+
<SettingsIcon />
16+
</Link>
17+
<div className="z-1 m-2.5 flex w-[210px] justify-center overflow-hidden rounded-full select-none">
18+
<img src={XEN_IMAGE} alt="xen-photo" className="h-full w-full" />
19+
</div>
20+
<Wallet />
21+
<HistoryMainPageModule />
22+
<Capabilities />
23+
</Layout>
24+
)
2525

2626
export default App
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const ArrayRightIcon = () => (
2+
<svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
3+
<path
4+
d="M1 9L5 5L1 1"
5+
stroke="currentColor"
6+
strokeWidth="1.5"
7+
strokeLinecap="round"
8+
strokeLinejoin="round"
9+
/>
10+
</svg>
11+
)
12+
13+
export default ArrayRightIcon
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { API_URL } from '@/env'
2+
import { useQuery } from '@tanstack/react-query'
3+
import { THistoryNote } from '../types'
4+
import Header from './Header'
5+
import HistoryNote from './HistoryNote'
6+
import Layout from './Layout'
7+
8+
const queryFn = (name: string, params?: { [key: string]: string | number }) => async () => {
9+
if (!window.Telegram.WebApp.initData) {
10+
throw new Error('Telegram is not available')
11+
}
12+
const response = await fetch(API_URL, {
13+
method: 'POST',
14+
headers: {
15+
Authorization: `Bearer ${window.Telegram.WebApp.initData}`,
16+
'Content-Type': 'application/json',
17+
},
18+
body: JSON.stringify({
19+
jsonrpc: '2.0',
20+
method: name,
21+
params: params ?? {},
22+
id: 1,
23+
}),
24+
})
25+
if (!response.ok) {
26+
throw new Error('Network response was not ok')
27+
}
28+
const data = await response.json()
29+
return data.result
30+
}
31+
32+
const History = () => {
33+
const { data: history } = useQuery<{ items: THistoryNote[]; total: number }>({
34+
queryKey: ['history'],
35+
queryFn: queryFn('getUsageHistory', {
36+
offset: 0,
37+
limit: 10,
38+
}),
39+
})
40+
41+
return (
42+
<Layout>
43+
<Header />
44+
<div className="z-1 flex w-full flex-col items-center justify-between gap-2.5 rounded-xl border border-[#f8f9ff66] p-2.5 backdrop-blur-3xl backdrop-opacity-80">
45+
<div className="my-1.5 flex w-full items-center justify-between">
46+
<h1 className="text-center text-2xl font-bold">Payment & Usage</h1>
47+
</div>
48+
{history?.items.map((note) => <HistoryNote key={note.id} note={note} />)}
49+
</div>
50+
</Layout>
51+
)
52+
}
53+
54+
export default History
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { API_URL } from '@/env'
2+
import { THistoryNote } from '@/types'
3+
import { useQuery } from '@tanstack/react-query'
4+
import { Link } from 'react-router'
5+
import ArrayRightIcon from '../assets/array-right'
6+
import { HistoryCard } from './HistoryNote'
7+
8+
const queryFn = (name: string, params?: { [key: string]: string | number }) => async () => {
9+
if (!window.Telegram.WebApp.initData) {
10+
throw new Error('Telegram is not available')
11+
}
12+
const response = await fetch(API_URL, {
13+
method: 'POST',
14+
headers: {
15+
Authorization: `Bearer ${window.Telegram.WebApp.initData}`,
16+
'Content-Type': 'application/json',
17+
},
18+
body: JSON.stringify({
19+
jsonrpc: '2.0',
20+
method: name,
21+
params: params ?? {},
22+
id: 1,
23+
}),
24+
})
25+
if (!response.ok) {
26+
throw new Error('Network response was not ok')
27+
}
28+
const data = await response.json()
29+
return data.result
30+
}
31+
32+
const HistoryMainPageModule = () => {
33+
const { data: history } = useQuery<{ items: THistoryNote[]; total: number }>({
34+
queryKey: ['history'],
35+
queryFn: queryFn('getUsageHistory', {
36+
offset: 0,
37+
limit: 1,
38+
}),
39+
})
40+
41+
return history?.total ? (
42+
<Link
43+
to="/history"
44+
className="z-1 flex w-full items-center justify-between gap-2.5 rounded-xl border border-[#f8f9ff66] p-2.5 text-(--color-main-text) backdrop-blur-3xl backdrop-opacity-80"
45+
>
46+
<HistoryCard note={history?.items[0]} />
47+
<div className="mr-6 flex cursor-pointer items-center justify-center py-1.5">
48+
<ArrayRightIcon />
49+
</div>
50+
</Link>
51+
) : null
52+
}
53+
54+
export default HistoryMainPageModule
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
Accordion,
3+
AccordionContent,
4+
AccordionItem,
5+
AccordionTrigger,
6+
} from '@/components/ui/accordion'
7+
import { formatNearAmount } from '@/lib/utils'
8+
import { FC } from 'react'
9+
import { THistoryNote } from '../types'
10+
11+
export const HistoryCard: FC<{ note: THistoryNote }> = ({ note }) => {
12+
const amountInNear = formatNearAmount(note.amount)
13+
const amountInNearRounded = amountInNear.slice(0, amountInNear.indexOf('.') + 4)
14+
return (
15+
<div className="flex items-center justify-between gap-2.5">
16+
<div className="flex w-max min-w-12 shrink-0 flex-col items-start gap-0.5">
17+
{note.isFree ? (
18+
<div className="flex py-0.25 text-[12px]/[100%] font-normal text-(--color-my-primary)">
19+
FREE
20+
</div>
21+
) : null}
22+
<div className="flex w-max shrink-0 py-0.25 text-[14px]/[150%] font-semibold">
23+
{(note.operationType === 'income' ? '+ ' : '- ') + amountInNearRounded}
24+
</div>
25+
</div>
26+
<div className="flex flex-col gap-0.5">
27+
<div className="flex py-0.25 text-[12px]/[100%] font-normal text-(--color-gray-text)">
28+
{new Date(note.createdAt).toLocaleString()}
29+
</div>
30+
<div className="flex py-0.25 text-[14px]/[150%] font-semibold wrap-anywhere">
31+
{note.capabilityName}
32+
</div>
33+
</div>
34+
</div>
35+
)
36+
}
37+
38+
type THistoryNoteProps = {
39+
note: THistoryNote
40+
}
41+
42+
const HistoryNote: FC<THistoryNoteProps> = ({ note }) => (
43+
<div className="flex w-full items-center justify-between gap-3.5 rounded-[10px] bg-(--color-light-white-bg) px-2.5 py-1.5 has-[[data-state='open']]:bg-[#ffffff7c] dark:has-[[data-state='open']]:bg-[#ffffff19]">
44+
<Accordion type="single" collapsible className="w-full">
45+
<AccordionItem value="item-1">
46+
<AccordionTrigger className="w-full cursor-pointer">
47+
<HistoryCard note={note} />
48+
</AccordionTrigger>
49+
<AccordionContent className="mt-2.5 flex flex-col gap-2.5 border-t-[1px] border-t-[#07070719] pt-2.5 dark:border-t-[#f8f9ff19]">
50+
<div>
51+
<div className="text-xs text-(--color-gray-text)">Execution input</div>
52+
<div>{note.executionInput}</div>
53+
</div>
54+
<div>
55+
<div className="text-xs text-(--color-gray-text)">Output</div>
56+
<div>{note.executionOutput}</div>
57+
</div>
58+
</AccordionContent>
59+
</AccordionItem>
60+
</Accordion>
61+
</div>
62+
)
63+
64+
export default HistoryNote

apps/xen-tg-app/src/components/Settings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const Settings = () => {
3737
queryKey: ['memories'],
3838
queryFn: queryFn('getMemories', {
3939
offset: 0,
40-
limit: 10,
40+
limit: 1,
4141
}),
4242
})
4343

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as React from 'react'
2+
import * as AccordionPrimitive from '@radix-ui/react-accordion'
3+
import { ChevronDownIcon } from 'lucide-react'
4+
5+
import { cn } from '@/lib/utils'
6+
7+
function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
8+
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
9+
}
10+
11+
function AccordionItem({
12+
className,
13+
...props
14+
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
15+
return (
16+
<AccordionPrimitive.Item
17+
data-slot="accordion-item"
18+
className={cn('border-b last:border-b-0', className)}
19+
{...props}
20+
/>
21+
)
22+
}
23+
24+
function AccordionTrigger({
25+
className,
26+
children,
27+
...props
28+
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
29+
return (
30+
<AccordionPrimitive.Header className="flex">
31+
<AccordionPrimitive.Trigger
32+
data-slot="accordion-trigger"
33+
className={cn(
34+
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-center justify-between gap-4 rounded-md py-0 text-left text-sm font-medium transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
35+
className
36+
)}
37+
{...props}
38+
>
39+
{children}
40+
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
41+
</AccordionPrimitive.Trigger>
42+
</AccordionPrimitive.Header>
43+
)
44+
}
45+
46+
function AccordionContent({
47+
className,
48+
children,
49+
...props
50+
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
51+
return (
52+
<AccordionPrimitive.Content
53+
data-slot="accordion-content"
54+
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
55+
{...props}
56+
>
57+
<div className={cn('p-0', className)}>{children}</div>
58+
</AccordionPrimitive.Content>
59+
)
60+
}
61+
62+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

apps/xen-tg-app/src/lib/utils.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,50 @@
1-
import { clsx, type ClassValue } from "clsx"
2-
import { twMerge } from "tailwind-merge"
1+
import { clsx, type ClassValue } from 'clsx'
2+
import { twMerge } from 'tailwind-merge'
33

44
export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs))
66
}
7+
8+
export const NEAR_NOMINATION_EXP = 24
9+
10+
// Pre-calculate offsets used for rounding to different number of digits
11+
const ROUNDING_OFFSETS: bigint[] = []
12+
const BN10 = 10n
13+
for (let i = 0, offset = 5n; i < NEAR_NOMINATION_EXP; i++, offset = offset * BN10) {
14+
ROUNDING_OFFSETS[i] = offset
15+
}
16+
17+
function trimTrailingZeroes(value: string): string {
18+
return value.replace(/\.?0*$/, '')
19+
}
20+
21+
function formatWithCommas(value: string): string {
22+
const pattern = /(-?\d+)(\d{3})/
23+
while (pattern.test(value)) {
24+
value = value.replace(pattern, '$1,$2')
25+
}
26+
return value
27+
}
28+
29+
export function formatNearAmount(
30+
balance: string,
31+
fracDigits: number = NEAR_NOMINATION_EXP
32+
): string {
33+
let balanceBN = BigInt(balance)
34+
if (fracDigits !== NEAR_NOMINATION_EXP) {
35+
// Adjust balance for rounding at given number of digits
36+
const roundingExp = NEAR_NOMINATION_EXP - fracDigits - 1
37+
if (roundingExp > 0) {
38+
balanceBN += ROUNDING_OFFSETS[roundingExp]
39+
}
40+
}
41+
42+
balance = balanceBN.toString()
43+
const wholeStr = balance.substring(0, balance.length - NEAR_NOMINATION_EXP) || '0'
44+
const fractionStr = balance
45+
.substring(balance.length - NEAR_NOMINATION_EXP)
46+
.padStart(NEAR_NOMINATION_EXP, '0')
47+
.substring(0, fracDigits)
48+
49+
return trimTrailingZeroes(`${formatWithCommas(wholeStr)}.${fractionStr}`)
50+
}

apps/xen-tg-app/src/main.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { StrictMode } from 'react'
44
import { createRoot } from 'react-dom/client'
55
import { createBrowserRouter, RouterProvider } from 'react-router'
66
import App from './App.tsx'
7+
import History from './components/History.tsx'
78
import Memories from './components/Memories.tsx'
8-
import './index.css'
99
import Settings from './components/Settings.tsx'
10+
import './index.css'
1011

1112
const queryClient = new QueryClient()
1213

@@ -23,6 +24,10 @@ const router = createBrowserRouter([
2324
path: 'settings',
2425
element: <Settings />,
2526
},
27+
{
28+
path: 'history',
29+
element: <History />,
30+
},
2631
])
2732

2833
createRoot(document.getElementById('root')!).render(

0 commit comments

Comments
 (0)