Skip to content

feat: tokens page revamp#1517

Open
rohanchkrabrty wants to merge 2 commits intomainfrom
feat-tokens-revamp
Open

feat: tokens page revamp#1517
rohanchkrabrty wants to merge 2 commits intomainfrom
feat-tokens-revamp

Conversation

@rohanchkrabrty
Copy link
Copy Markdown
Contributor

Summary

  • Add new tokens page in views-new/tokens/ using apsara-v1 components (DataTable, Dialog, Callout, etc.)
  • Implement virtualized transaction table with date and event filters, token amount display, and member column with avatars
  • Add tokens dialog with form validation, product pricing display, and Stripe checkout integration
  • Wire up tokens page in client-demo app router and settings sidebar

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Apr 8, 2026 4:30am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c60d2644-0957-4238-bd8d-b12da4bc1195

📥 Commits

Reviewing files that changed from the base of the PR and between 38d385e and d399e8c.

📒 Files selected for processing (1)
  • web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • New Tokens section in Settings for managing token balance and purchases.
    • Token balance panel showing available balance with loading states.
    • Add Tokens dialog to purchase tokens and proceed to checkout.
    • Transaction history table with sortable/filterable Date, Tokens, Event, and Member columns.
    • Permissions-aware controls and user-facing errors/toasts for failed data loads.

Walkthrough

Adds a new Tokens settings UI: a TokensView component with token balance, transactions table, and Add Tokens dialog; wires the view into the client-demo app routing and sidebar; and exposes TokensView through the React SDK exports and supporting files.

Changes

Cohort / File(s) Summary
Client-demo routing & page
web/apps/client-demo/src/Router.tsx, web/apps/client-demo/src/pages/Settings.tsx, web/apps/client-demo/src/pages/settings/Tokens.tsx
Registered /:orgId/settings/tokens route, added "Tokens" to NAV_ITEMS, and added a wrapper page that renders the SDK TokensView.
Tokens view & styles
web/sdk/react/views-new/tokens/tokens-view.tsx, web/sdk/react/views-new/tokens/tokens-view.module.css
New TokensView component showing balance, conditional callout, Add Tokens trigger, and transactions DataTable; added CSS module for layout and visual styles.
Add tokens dialog
web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx
New dialog component implementing product fetch, validation (Yup + react-hook-form), quantity bounds, checkout mutation, and redirect on success.
Transactions table columns
web/sdk/react/views-new/tokens/components/columns.tsx
New column definitions for transactions DataTable: Date (with filters), Tokens (signed amounts), Event (multiselect filter), and Member (avatar/title).
SDK exports / barrels
web/sdk/react/index.ts, web/sdk/react/views-new/tokens/index.ts
Exported TokensView from the tokens view index and added it to the main React SDK barrel.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
  • rsbh
  • paanSinghCoder

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 7, 2026

Coverage Report for CI Build 24117771025

Coverage remained the same at 41.146%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 36298
Covered Lines: 14935
Line Coverage: 41.15%
Coverage Strength: 11.89 hits per line

💛 - Coveralls

@rohanchkrabrty rohanchkrabrty changed the title feat: revamp tokens page with apsara-v1 components feat: tokens page revamp Apr 7, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
web/sdk/react/views-new/tokens/tokens-view.tsx (1)

18-19: Consider consolidating Radix icon imports.

These two imports from the same package can be merged into a single import statement.

-import { InfoCircledIcon, PlusIcon } from '@radix-ui/react-icons';
-import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
+import { InfoCircledIcon, PlusIcon, ExclamationTriangleIcon } from '@radix-ui/react-icons';

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 37a7c21e-aac2-4e3d-b4ea-7a09d1629893

📥 Commits

Reviewing files that changed from the base of the PR and between 862e133 and 38d385e.

📒 Files selected for processing (9)
  • web/apps/client-demo/src/Router.tsx
  • web/apps/client-demo/src/pages/Settings.tsx
  • web/apps/client-demo/src/pages/settings/Tokens.tsx
  • web/sdk/react/index.ts
  • web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx
  • web/sdk/react/views-new/tokens/components/columns.tsx
  • web/sdk/react/views-new/tokens/index.ts
  • web/sdk/react/views-new/tokens/tokens-view.module.css
  • web/sdk/react/views-new/tokens/tokens-view.tsx

Comment on lines +55 to +63
const tokensSchema = yup
.object({
tokens: yup
.number()
.required('Please enter valid number')
.min(minQuantity, `Minimum ${minQuantity} token is required`)
.max(maxQuantity, `Maximum ${maxQuantity} tokens are allowed`)
.typeError('Please enter valid number of tokens')
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify schema vs BigInt conversion mismatch.
rg -n "yup\\.number\\(|\\.integer\\(|BigInt\\(data\\.tokens\\)" web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx -n -C2

Repository: raystack/frontier

Length of output: 207


🏁 Script executed:

cat -n web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx | sed -n '50,120p'

Repository: raystack/frontier

Length of output: 2600


Add .integer() validator to prevent decimal input before BigInt conversion.

The validation schema at lines 55-63 uses yup.number(), which accepts decimals (e.g., 1.5). However, line 117 converts the value with BigInt(data.tokens), which throws a RangeError for non-integer numbers. Add the .integer() validator to enforce integer-only input:

Proposed fix
       tokens: yup
         .number()
         .required('Please enter valid number')
+        .integer('Please enter a whole number of tokens')
         .min(minQuantity, `Minimum ${minQuantity} token is required`)
         .max(maxQuantity, `Maximum ${maxQuantity} tokens are allowed`)
         .typeError('Please enter valid number of tokens')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tokensSchema = yup
.object({
tokens: yup
.number()
.required('Please enter valid number')
.min(minQuantity, `Minimum ${minQuantity} token is required`)
.max(maxQuantity, `Maximum ${maxQuantity} tokens are allowed`)
.typeError('Please enter valid number of tokens')
})
const tokensSchema = yup
.object({
tokens: yup
.number()
.required('Please enter valid number')
.integer('Please enter a whole number of tokens')
.min(minQuantity, `Minimum ${minQuantity} token is required`)
.max(maxQuantity, `Maximum ${maxQuantity} tokens are allowed`)
.typeError('Please enter valid number of tokens')
})

Comment on lines +95 to +106
const query = qs.stringify(
{
details: btoa(
qs.stringify({
organization_id: activeOrganization?.id,
type: 'tokens'
})
),
checkout_id: '{{.CheckoutID}}'
},
{ encode: false }
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current query construction/encoding settings.
rg -n "btoa\\(|qs\\.stringify\\(|encode:\\s*false|checkout_id" web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx -n -C2

Repository: raystack/frontier

Length of output: 527


🏁 Script executed:

# Search for other qs.stringify usages in the file and across the codebase
rg "qs\.stringify" web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx -B2 -A3

Repository: raystack/frontier

Length of output: 308


🏁 Script executed:

# Check for server-side handler of checkout or billing parameters
fd -e "go|ts|tsx|js" | xargs rg -l "CheckoutID|checkout.*details|details.*btoa" | head -20

Repository: raystack/frontier

Length of output: 856


🏁 Script executed:

# Search for any tests related to add-tokens-dialog
fd "add-tokens-dialog" web/sdk/react --type f

Repository: raystack/frontier

Length of output: 173


🏁 Script executed:

# Check if there are comments or issues about encoding in this file
rg -i "encod|query|checkout" web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx -B1 -A1

Repository: raystack/frontier

Length of output: 1376


🏁 Script executed:

# Check the old version of add-tokens-dialog to see if it also has this issue
cat -n web/sdk/react/views/tokens/add-tokens-dialog.tsx | sed -n '80,120p'

Repository: raystack/frontier

Length of output: 1529


🏁 Script executed:

# Look at server-side checkout service to understand how it processes details param
cat -n billing/checkout/service.go | head -100

Repository: raystack/frontier

Length of output: 4078


🏁 Script executed:

# Search for how details parameter is parsed/used in checkout tests
rg "details.*btoa|CheckoutID|query.*details" test/e2e/regression/billing_test.go -B3 -A3

Repository: raystack/frontier

Length of output: 484


🏁 Script executed:

# Check qs library behavior - look for any documentation or usage patterns
rg "query-string|qs\." web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx -B5 -A5

Repository: raystack/frontier

Length of output: 882


🏁 Script executed:

# Search for how the details parameter is parsed on server side
rg "details|Details" billing/checkout/service.go -B2 -A2 | head -50

Repository: raystack/frontier

Length of output: 406


🏁 Script executed:

# Look for query parameter handling or URL parsing in checkout
rg -i "query|param|details" billing/checkout/service.go | head -20

Repository: raystack/frontier

Length of output: 1283


🏁 Script executed:

# Check the old version behavior - see if it also uses encode: false
rg -A5 "qs.stringify" web/sdk/react/views/tokens/add-tokens-dialog.tsx | grep -A5 "encode"

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Create a simple test to understand btoa output and qs.stringify behavior
python3 << 'EOF'
import base64
import json
import urllib.parse

# Simulate what btoa does in the code
inner_data = {"organization_id": "test-org-123", "type": "tokens"}
inner_json = json.dumps(inner_data, separators=(',', ':'))
print(f"Inner JSON: {inner_json}")

# Simulate btoa (base64 encode)
base64_encoded = base64.b64encode(inner_json.encode()).decode()
print(f"Base64 encoded: {base64_encoded}")
print(f"Contains special chars: +={'+' in base64_encoded}, /={('/' in base64_encoded)}, =={('=' in base64_encoded)}")

# Simulate with encode: false (no URL encoding)
query_unencoded = f"details={base64_encoded}&checkout_id={'{{'}.CheckoutID{'}}'}"
print(f"\nQuery with encode: false:\n{query_unencoded}")

# Simulate with encode: true (URL encoding)
query_encoded = f"details={urllib.parse.quote(base64_encoded, safe='')}&checkout_id={'{{'}.CheckoutID{'}}'}"
print(f"\nQuery with encode: true:\n{query_encoded}")

# What happens when parsing unencoded version
print(f"\nWhen server receives unencoded query:")
parsed = urllib.parse.parse_qs(query_unencoded)
print(f"Parsed details value: {parsed.get('details', [''])}")
print(f"Matches original base64: {parsed.get('details', [''])[0] == base64_encoded}")
EOF

Repository: raystack/frontier

Length of output: 686


🏁 Script executed:

# Search for any related issues or TODOs about encoding/checkout
rg -i "TODO|FIXME|BUG|issue" web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Check if there are tests that verify the checkout URL encoding
fd -e test.tsx -e test.ts web/sdk/react/views-new/tokens/ 2>/dev/null

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Look for how the query string is actually used - check if Stripe docs mention encoding
rg "successUrl|cancelUrl|checkoutUrl" web/sdk/react/views-new/tokens/components/add-tokens-dialog.tsx -B2 -A2

Repository: raystack/frontier

Length of output: 735


🏁 Script executed:

# Check if qs.stringify with encode: true is used elsewhere in the codebase for comparison
rg "qs\.stringify.*encode" web/sdk/react/ -B1 -A1 | head -40

Repository: raystack/frontier

Length of output: 43


Use encode: true (or remove the option) for proper URL encoding of base64-encoded query parameters.

The { encode: false } setting bypasses URL encoding, leaving base64 special characters unencoded. The = padding character remains in the query string, but more critically, + characters become spaces and / characters can corrupt parsing. This violates URL encoding standards and risks breaking checkout redirects when base64 output contains these characters.

💡 Proposed fix
      const query = qs.stringify(
        {
          details: btoa(
            qs.stringify({
              organization_id: activeOrganization?.id,
              type: 'tokens'
            })
          ),
          checkout_id: '{{.CheckoutID}}'
        },
-        { encode: false }
+        { encode: true }
       );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const query = qs.stringify(
{
details: btoa(
qs.stringify({
organization_id: activeOrganization?.id,
type: 'tokens'
})
),
checkout_id: '{{.CheckoutID}}'
},
{ encode: false }
);
const query = qs.stringify(
{
details: btoa(
qs.stringify({
organization_id: activeOrganization?.id,
type: 'tokens'
})
),
checkout_id: '{{.CheckoutID}}'
},
{ encode: true }
);

Comment on lines +77 to +83
const value = getValue() as bigint;
const prefix = row?.original?.type === 'credit' ? '+' : '-';
return (
<Text size="regular" variant="secondary">
{prefix}
{Number(value)}
</Text>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify int64 rendering path and risky bigint->number conversion.
rg -n "accessorKey: 'amount'|Number\\(value\\)|as bigint|BillingTransaction" web/sdk/react/views-new/tokens/components/columns.tsx -n -C2

Repository: raystack/frontier

Length of output: 972


🏁 Script executed:

# 1. Find BillingTransaction type definition and amount field type
rg "type BillingTransaction|interface BillingTransaction|message BillingTransaction" -A 10

# 2. Check for other Number(bigint) conversions in the codebase
rg "Number\([a-zA-Z_][a-zA-Z0-9_]*\)\s*(?://|$)" --type ts --type tsx | head -20

# 3. Look for similar getValue() patterns with bigint casting
rg "getValue\(\)\s*as\s*bigint" --type ts --type tsx

Repository: raystack/frontier

Length of output: 2070


🏁 Script executed:

# Search for TypeScript/JavaScript files with Number(bigint) pattern
rg "Number\([a-zA-Z_]" web/sdk/react/ -t js | head -20

# Look for other getValue() patterns with type casting
rg "getValue\(\).*as" web/sdk/react/views-new/tokens/components/ -A 1

# Check for any token amount limits or documentation
rg "token|amount" web/sdk/react/views-new/tokens/ -i -l

# Find any .d.ts or type files related to BillingTransaction
fd "\.d\.ts|types\." | xargs rg "BillingTransaction" 2>/dev/null | head -20

Repository: raystack/frontier

Length of output: 1059


🏁 Script executed:

# Find TypeScript type definitions - check common generated type locations
fd -e ts -e d.ts web/sdk/ | xargs rg "BillingTransaction" 2>/dev/null

# Search entire codebase for Number() conversions on potentially large numbers
rg "Number\([a-zA-Z_][a-zA-Z0-9_]*\)" web/ --type ts -B 2 -A 1 | head -40

# Check package.json to understand proto generation setup
rg "@raystack/proton" web/sdk/ -A 2 -B 2

# Look at imports in the columns file more carefully
head -10 web/sdk/react/views-new/tokens/components/columns.tsx

Repository: raystack/frontier

Length of output: 50729


🏁 Script executed:

# Check which token view files are being changed in this PR context
# Look for the old view's current state to compare
cat -n web/sdk/react/views/tokens/transactions/columns.tsx | grep -A 10 "Number(value)"

# Check if there are any constants or tests defining max token amounts
rg "MAX.*AMOUNT|max.*token|token.*limit" web/ -i --max-count 10

# Look at imports and type definition for clarity on what getValue returns
sed -n '70,85p' web/sdk/react/views-new/tokens/components/columns.tsx

Repository: raystack/frontier

Length of output: 1284


Use bigint.toString() instead of Number() to prevent precision loss for large token amounts.

Line 82 converts bigint to Number, which loses precision for values exceeding 9,007,199,254,740,991. Since BillingTransaction.amount is int64 (max 9,223,372,036,854,775,807), large token balances will display incorrectly. Render as a string instead.

Proposed fix
         const value = getValue() as bigint;
         const prefix = row?.original?.type === 'credit' ? '+' : '-';
         return (
           <Text size="regular" variant="secondary">
             {prefix}
-            {Number(value)}
+            {value.toString()}
           </Text>
         );

import { AddTokensDialog } from './components/add-tokens-dialog';
import styles from './tokens-view.module.css';

const addTokensDialogHandle = Dialog.createHandle();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Module-level dialog handle creates shared state across component instances.

Dialog.createHandle() at module scope means all TokensView instances share the same dialog state. This causes issues if:

  1. Multiple TokensView components are rendered simultaneously
  2. During SSR, state may leak between requests
  3. Hot module replacement may preserve stale handles

Move the handle creation inside the component using useMemo or a ref.

Proposed fix
-const addTokensDialogHandle = Dialog.createHandle();
-
 export interface TokensViewProps {}

 export function TokensView(_props: TokensViewProps = {}) {
+  const addTokensDialogHandle = useMemo(() => Dialog.createHandle(), []);
+
   const {

Note: You'll need to add useMemo to the existing import from react if not already present (it is imported on line 3).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const addTokensDialogHandle = Dialog.createHandle();
export interface TokensViewProps {}
export function TokensView(_props: TokensViewProps = {}) {
const addTokensDialogHandle = useMemo(() => Dialog.createHandle(), []);
const {

Comment on lines +106 to +107
const isPostpaid =
billingDetails?.creditMin && billingDetails.creditMin < BigInt(0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for how creditMin is used elsewhere to understand the expected semantics
rg -n "creditMin" --type ts --type tsx -C 3

Repository: raystack/frontier

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Search for creditMin usage in TypeScript files
rg -n "creditMin" -t ts -C 3

Repository: raystack/frontier

Length of output: 9735


🏁 Script executed:

#!/bin/bash
# Also search in all files to catch any .tsx or other extensions
rg -n "creditMin" -C 3

Repository: raystack/frontier

Length of output: 12308


Simplify postpaid detection to be consistent with other usages.

Change line 106-107 to use direct comparison:

const isPostpaid = billingDetails?.creditMin? < BigInt(0);

The current billingDetails?.creditMin && billingDetails.creditMin < BigInt(0) pattern is redundant. The optional chaining (?.) already handles null/undefined cases. Throughout the codebase (e.g., billing.tsx line 98, tokens-details-section.tsx line 85), postpaid detection uses direct comparison (creditMin < 0n or Number(creditMin) < 0). Adopt the same pattern for consistency.

Copy link
Copy Markdown
Contributor

@paanSinghCoder paanSinghCoder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with minor comments.

import { Avatar, Flex, Text, getAvatarColor } from '@raystack/apsara-v1';
import type { DataTableColumnDef } from '@raystack/apsara-v1';
import type { BillingTransaction } from '@raystack/proton/frontier';
import * as _ from 'lodash';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets avoid * import.


const addTokensDialogHandle = Dialog.createHandle();

export interface TokensViewProps {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export interface TokensViewProps {} Remove if not used anywhere.

} from '@raystack/apsara-v1';
import { toastManager } from '@raystack/apsara-v1';
import { InfoCircledIcon, PlusIcon } from '@radix-ui/react-icons';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { InfoCircledIcon, PlusIcon, ExclamationTriangleIcon } from '@radix-ui/react-icons'; Import can be merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants