diff --git a/app/src/components/BalanceCodesSection.tsx b/app/src/components/BalanceCodesSection.tsx index 97a7454..d6d19ab 100644 --- a/app/src/components/BalanceCodesSection.tsx +++ b/app/src/components/BalanceCodesSection.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, useCallback } from "react"; import { useAuth } from "../hooks/useAuth"; import { Ticket, TicketCheck, TicketSlash } from "lucide-react"; -import { RedeemedTransferBalanceCode } from "../services/types"; -import { fetchNetworkTransferBalanceCodes } from "../services/api"; +import { RedeemedTransferBalanceCode, SubscriptionBalanceResponse } from "../services/types"; +import { fetchNetworkTransferBalanceCodes, fetchSubscriptionBalance } from "../services/api"; import RedeemTransferBalanceCodeModal from "./RedeemTransferBalanceCodeModal"; const BalanceCodesSection: React.FC = () => { @@ -53,7 +53,9 @@ const BalanceCodesSection: React.FC = () => { const [transferBalanceCodes, setTransferBalanceCodes] = useState([]); const [isAddTransferBalanceCodeModalOpen, setIsAddTransferBalanceCodeModalOpen] = useState(false); const [isLoadingTransferBalanceCodes, setIsLoadingTransferBalanceCodes] = useState(true); - + const [isLoadingSubscriptionBalance, setIsLoadingSubscriptionBalance] = useState(true); + const [subscriptionBalance, setSubscriptionBalance] = useState(null); + const loadTransferBalanceCodes = useCallback(async () => { if (!token) { setIsLoadingTransferBalanceCodes(false); @@ -74,9 +76,34 @@ const BalanceCodesSection: React.FC = () => { } }, [token]); + const loadSubscriptionBalance = useCallback(async () => { + + if (!token) { + setIsLoadingSubscriptionBalance(false); + return; + } + + setIsLoadingSubscriptionBalance(true); + + try { + const response = await fetchSubscriptionBalance(token); + if (response && 'error' in response) { + console.error('Error fetching subscription balance:', response.error.message); + return; + } + setSubscriptionBalance(response); + } catch (error) { + console.error('Failed to fetch subscription balance:', error); + } finally { + setIsLoadingSubscriptionBalance(false); + } + + }, [token]); + useEffect(() => { loadTransferBalanceCodes(); - }, [token, loadTransferBalanceCodes]); + loadSubscriptionBalance(); + }, [token, loadTransferBalanceCodes, loadSubscriptionBalance]); const maskSecret = (secret: string) => { if (!secret || secret.length <= 6) return secret; @@ -88,6 +115,19 @@ const BalanceCodesSection: React.FC = () => { return date.toLocaleString(); }; + const formatDataBalance = (bytes: number) => { + if (typeof bytes !== "number" || isNaN(bytes)) return "-"; + const TIB = 1099511627776; + const GIB = 1073741824; + if (bytes < TIB) { + const gib = bytes / GIB; + return `${gib.toFixed(2)} GiB`; + } else { + const tib = bytes / TIB; + return `${tib.toFixed(2)} TiB`; + } + }; + return (
@@ -104,7 +144,6 @@ const BalanceCodesSection: React.FC = () => {
-
@@ -116,20 +155,38 @@ const BalanceCodesSection: React.FC = () => {
+
+

+ Total Data Balance: + + {isLoadingSubscriptionBalance + ? " Loading..." + : subscriptionBalance + ? formatDataBalance(subscriptionBalance.balance_byte_count) + : "-" + } + +

+
+ {transferBalanceCodes.length > 0 ? (
+ + {transferBalanceCodes.map((code) => ( + + ))} diff --git a/app/src/services/api.ts b/app/src/services/api.ts index ce644c2..ecd586e 100644 --- a/app/src/services/api.ts +++ b/app/src/services/api.ts @@ -34,6 +34,7 @@ import type { NetworkReliabilityResponse, RedeemedTransferBalanceCodesResponse, RedeemTransferBalanceCodeResponse, + SubscriptionBalanceResponse, } from "./types"; const API_BASE_URL = import.meta.env.VITE_API_BASE ?? "https://api.bringyour.com"; @@ -980,6 +981,48 @@ export const redeemTransferBalanceCode = async ( } }; +/** + * Get subscription balance + * @param token - JWT authentication token + * @returns SubscriptionBalanceResponse with balance info + */ +export const fetchSubscriptionBalance = async ( + token: string +): Promise => { + try { + const response = await fetch(`${API_BASE_URL}/subscription/balance`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + Accept: "*/*", + }, + }); + + if (!response.ok) { + return { + error: { + message: `HTTP error! status: ${response.status}`, + }, + }; + } + + const data = await safeJsonParse(response); + + return data; + + } catch (error) { + console.error("Fetch subscription balance error:", error); + return { + error: { + message: + error instanceof Error + ? error.message + : "Failed to fetch subscription balance", + }, + }; + } +}; + // Export types for convenience export type { AuthResponse, diff --git a/app/src/services/types.ts b/app/src/services/types.ts index f543cc5..2d0bf20 100644 --- a/app/src/services/types.ts +++ b/app/src/services/types.ts @@ -562,4 +562,33 @@ export interface RedeemedTransferBalanceCodesResponse { */ export interface RedeemTransferBalanceCodeResponse { error?: { message: string }; +} + +/** + * Response from fetching subscription balance + */ +export interface SubscriptionBalanceResponse { + start_balance_byte_count: number; + balance_byte_count: number; + open_transfer_byte_count: number; + current_subscription: CurrentSubscription; + pending_payout_usd_nano_cents: number; + update_time: string; + active_transfer_balances: TransferBalance[]; +} + +export interface CurrentSubscription { + subscription_id: string; + store: string; // e.g., "apple", "google" + plan: string; +} + +export interface TransferBalance { + balance_id: string; + network_id: string; + start_time: string; + end_time: string; + start_balance_byte_count: number; + net_revenue: number; + balance_byte_count: number; } \ No newline at end of file
SecretData RedeemedValid Until
{maskSecret(code.secret)}{`+${formatDataBalance(code.balance_byte_count)}`} {formatDate(code.redeem_time)}{formatDate(code.end_time)}