Skip to content

Commit c795a46

Browse files
authored
feat(sdk): add tokens screen (#533)
* feat: add tokens page * feat: add tokens route to the router * feat: add tokens route to sidebar * feat: implement balance api to token screen * chore: show loading in headers * refactor: move getFormattedNumberString to utils * chore: show toast if api fetch fails
1 parent 1839271 commit c795a46

File tree

7 files changed

+201
-8
lines changed

7 files changed

+201
-8
lines changed
Lines changed: 11 additions & 0 deletions
Loading

sdks/js/packages/core/react/components/organization/profile.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { SkeletonTheme } from 'react-loading-skeleton';
3636
import { InviteTeamMembers } from './teams/members/invite';
3737
import { DeleteDomain } from './domain/delete';
3838
import Billing from './billing';
39+
import Tokens from './tokens';
3940
import { EditBillingAddress } from './billing/address/edit';
4041
import Plans from './plans';
4142
import ConfirmPlanChange from './plans/confirm-change';
@@ -44,10 +45,14 @@ interface OrganizationProfileProps {
4445
organizationId: string;
4546
defaultRoute?: string;
4647
tempShowBilling?: boolean;
48+
tempShowTokens?: boolean;
4749
}
4850

4951
const routerContext = new RouterContext<
50-
Pick<OrganizationProfileProps, 'organizationId' | 'tempShowBilling'>
52+
Pick<
53+
OrganizationProfileProps,
54+
'organizationId' | 'tempShowBilling' | 'tempShowTokens'
55+
>
5156
>();
5257

5358
const RootRouter = () => {
@@ -246,6 +251,12 @@ const planDowngradeRoute = new Route({
246251
component: ConfirmPlanChange
247252
});
248253

254+
const tokensRoute = new Route({
255+
getParentRoute: () => rootRoute,
256+
path: '/tokens',
257+
component: Tokens
258+
});
259+
249260
const routeTree = rootRoute.addChildren([
250261
indexRoute.addChildren([deleteOrgRoute]),
251262
securityRoute,
@@ -262,18 +273,20 @@ const routeTree = rootRoute.addChildren([
262273
profileRoute,
263274
preferencesRoute,
264275
billingRoute.addChildren([editBillingAddressRoute]),
265-
plansRoute.addChildren([planDowngradeRoute])
276+
plansRoute.addChildren([planDowngradeRoute]),
277+
tokensRoute
266278
]);
267279

268280
const router = new Router({
269281
routeTree,
270-
context: { organizationId: '', tempShowBilling: false }
282+
context: { organizationId: '', tempShowBilling: false, tempShowTokens: false }
271283
});
272284

273285
export const OrganizationProfile = ({
274286
organizationId,
275287
defaultRoute = '/',
276-
tempShowBilling = false
288+
tempShowBilling = false,
289+
tempShowTokens = false
277290
}: OrganizationProfileProps) => {
278291
const memoryHistory = createMemoryHistory({
279292
initialEntries: [defaultRoute]
@@ -282,7 +295,7 @@ export const OrganizationProfile = ({
282295
const memoryRouter = new Router({
283296
routeTree,
284297
history: memoryHistory,
285-
context: { organizationId, tempShowBilling }
298+
context: { organizationId, tempShowBilling, tempShowTokens }
286299
});
287300

288301
return <RouterProvider router={memoryRouter} />;

sdks/js/packages/core/react/components/organization/sidebar/helpers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type NavigationItemsTypes = {
77

88
interface getOrganizationNavItemsOptions {
99
tempShowBilling?: boolean;
10+
tempShowTokens?: boolean;
1011
canSeeBilling?: boolean;
1112
}
1213

@@ -44,6 +45,11 @@ export const getOrganizationNavItems = (
4445
to: '/billing',
4546
show: options?.tempShowBilling && options?.canSeeBilling
4647
},
48+
{
49+
name: 'Tokens',
50+
to: '/tokens',
51+
show: options?.tempShowTokens
52+
},
4753
{
4854
name: 'Plans',
4955
to: '/plans',

sdks/js/packages/core/react/components/organization/sidebar/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import styles from './sidebar.module.css';
2121
export const Sidebar = () => {
2222
const [search, setSearch] = useState('');
2323
const routerState = useRouterState();
24-
const { organizationId, tempShowBilling } = useRouterContext({
24+
const { organizationId, tempShowBilling, tempShowTokens } = useRouterContext({
2525
from: '__root__'
2626
});
2727

@@ -62,9 +62,10 @@ export const Sidebar = () => {
6262
() =>
6363
getOrganizationNavItems({
6464
tempShowBilling: tempShowBilling,
65-
canSeeBilling: canSeeBilling
65+
canSeeBilling: canSeeBilling,
66+
tempShowTokens: tempShowTokens
6667
}),
67-
[tempShowBilling, canSeeBilling]
68+
[tempShowBilling, canSeeBilling, tempShowTokens]
6869
);
6970

7071
return (
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { Flex, Image, Text } from '@raystack/apsara';
2+
import { styles } from '../styles';
3+
import Skeleton from 'react-loading-skeleton';
4+
import tokenStyles from './token.module.css';
5+
import { useFrontier } from '~/react/contexts/FrontierContext';
6+
import { useEffect, useState } from 'react';
7+
import coin from '~/react/assets/coin.svg';
8+
import { getFormattedNumberString } from '~/react/utils';
9+
import { toast } from 'sonner';
10+
11+
interface TokenHeaderProps {
12+
billingSupportEmail?: string;
13+
isLoading?: boolean;
14+
}
15+
16+
const TokensHeader = ({ billingSupportEmail, isLoading }: TokenHeaderProps) => {
17+
return (
18+
<Flex direction="column" gap="small">
19+
{isLoading ? (
20+
<Skeleton containerClassName={tokenStyles.flex1} />
21+
) : (
22+
<Text size={6}>Tokens</Text>
23+
)}
24+
{isLoading ? (
25+
<Skeleton containerClassName={tokenStyles.flex1} />
26+
) : (
27+
<Text size={4} style={{ color: 'var(--foreground-muted)' }}>
28+
Oversee your billing and invoices.
29+
{billingSupportEmail ? (
30+
<>
31+
{' '}
32+
For more details, contact{' '}
33+
<a
34+
href={`mailto:${billingSupportEmail}`}
35+
target="_blank"
36+
style={{ fontWeight: 400, color: 'var(--foreground-accent)' }}
37+
>
38+
{billingSupportEmail}
39+
</a>
40+
</>
41+
) : null}
42+
</Text>
43+
)}
44+
</Flex>
45+
);
46+
};
47+
48+
interface BalancePanelProps {
49+
balance: number;
50+
isLoading: boolean;
51+
}
52+
53+
function BalancePanel({ balance, isLoading }: BalancePanelProps) {
54+
const formattedBalance = getFormattedNumberString(balance);
55+
return (
56+
<Flex className={tokenStyles.balancePanel}>
57+
<Flex className={tokenStyles.balanceTokenBox}>
58+
{/* @ts-ignore */}
59+
<Image src={coin} alt="coin" className={tokenStyles.coinIcon} />
60+
<Flex direction={'column'} gap={'extra-small'}>
61+
<Text weight={500} style={{ color: 'var(--foreground-muted)' }}>
62+
Available tokens
63+
</Text>
64+
{isLoading ? (
65+
<Skeleton style={{ height: '24px' }} />
66+
) : (
67+
<Text size={9} weight={600}>
68+
{formattedBalance}
69+
</Text>
70+
)}
71+
</Flex>
72+
</Flex>
73+
</Flex>
74+
);
75+
}
76+
77+
export default function Tokens() {
78+
const {
79+
config,
80+
client,
81+
activeOrganization,
82+
billingAccount,
83+
isActiveOrganizationLoading,
84+
isBillingAccountLoading
85+
} = useFrontier();
86+
const [tokenBalance, setTokenBalance] = useState(0);
87+
const [isTokensLoading, setIsTokensLoading] = useState(false);
88+
89+
useEffect(() => {
90+
async function getBalance(orgId: string, billingAccountId: string) {
91+
try {
92+
setIsTokensLoading(true);
93+
const resp = await client?.frontierServiceGetBillingBalance(
94+
orgId,
95+
billingAccountId
96+
);
97+
const tokens = resp?.data?.balance?.amount || '0';
98+
setTokenBalance(Number(tokens));
99+
} catch (err: any) {
100+
console.error(err);
101+
toast.error('Unable to fetch balance');
102+
} finally {
103+
setIsTokensLoading(false);
104+
}
105+
}
106+
107+
if (activeOrganization?.id && billingAccount?.id) {
108+
getBalance(activeOrganization?.id, billingAccount?.id);
109+
}
110+
}, [activeOrganization?.id, billingAccount?.id, client]);
111+
112+
const isLoading =
113+
isActiveOrganizationLoading || isBillingAccountLoading || isTokensLoading;
114+
115+
return (
116+
<Flex direction="column" style={{ width: '100%' }}>
117+
<Flex style={styles.header}>
118+
<Text size={6}>Tokens</Text>
119+
</Flex>
120+
<Flex direction="column" gap="large" style={styles.container}>
121+
<Flex direction="column" gap={'large'}>
122+
<TokensHeader
123+
billingSupportEmail={config.billing?.supportEmail}
124+
isLoading={isLoading}
125+
/>
126+
<BalancePanel balance={tokenBalance} isLoading={isLoading} />
127+
</Flex>
128+
</Flex>
129+
</Flex>
130+
);
131+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.flex1 {
2+
flex: 1;
3+
}
4+
5+
.coinIcon {
6+
height: 48px;
7+
width: 48px;
8+
}
9+
10+
.balancePanel {
11+
padding: var(--pd-16);
12+
border-radius: var(--br-4);
13+
border: 1px solid var(--border-base);
14+
}
15+
16+
.balanceTokenBox {
17+
gap: 12px;
18+
}

sdks/js/packages/core/react/utils/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,16 @@ export const getPlanChangeAction = (
7070
};
7171
}
7272
};
73+
74+
export function getFormattedNumberString(num: Number = 0) {
75+
const numString = num.toString();
76+
const length = numString.length;
77+
78+
return numString.split('').reduce((acc, val, i) => {
79+
const diff = length - i;
80+
if (diff % 3 === 0 && diff < length) {
81+
return acc + ',' + val;
82+
}
83+
return acc + val;
84+
}, '');
85+
}

0 commit comments

Comments
 (0)