Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions desktop/renderer/src/ui/settings/atomic/AtomicAccountTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,26 +153,6 @@ export function AtomicAccountTab(props: { port: number }) {
)}
</div> */}
</div>

{balanceDepleted && (
<div className={s.depletedCard}>
<div className={s.depletedBody}>
<div className={s.depletedTitle}>No credits left</div>
<div className={s.depletedSubtitle}>
Top up to continue using AI.
</div>
</div>
<button
type="button"
className={s.depletedAction}
onClick={() => void handleTopUp()}
disabled={topupBusy}
>
{topupBusy ? "Opening…" : "Top up"}
</button>
</div>
)}

{errorMessage && <div className={s.errorRow}>{errorMessage}</div>}
</div>

Expand Down
2 changes: 1 addition & 1 deletion desktop/renderer/src/ui/setup/atomic/AtomicTopupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
import { routes } from "../../app/routes";
import s from "./AtomicTopupPage.module.css";

const DEFAULT_TOPUP_AMOUNT_USD = 25;
const DEFAULT_TOPUP_AMOUNT_USD = 10;

const FEATURES = [
"Credits never expire",
Expand Down
69 changes: 23 additions & 46 deletions desktop/renderer/src/ui/shared/atomic/LowBalanceBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
import React from "react";
import { useAppDispatch, useAppSelector } from "@store/hooks";
import {
atomicAuthActions,
fetchAtomicBalance,
} from "@store/slices/atomicAuthSlice";
import {
STRIPE_PAYG_CANCEL_URL,
atomicBackendApi,
getStripePaygSuccessUrl,
} from "../../../services/atomic-backend-api";
import { fetchAtomicBalance } from "@store/slices/atomicAuthSlice";
import s from "./LowBalanceBanner.module.css";
import { useNavigate } from "react-router-dom";
import { routes } from "@ui/app/routes";

const DEFAULT_THRESHOLD_USD = 1;
const SESSION_DISMISS_KEY = "hermes:low-balance-banner-dismissed";
const DEFAULT_TOPUP_USD = 10;

function openExternal(url: string): void {
const api = (window as { hermesAPI?: { openExternal?: (u: string) => void } }).hermesAPI;
if (api?.openExternal) {
void api.openExternal(url);
return;
}
window.open(url, "_blank");
}

function EmptyWalletIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 7v4m0 2.5h.008M4.34 17h11.32c1.34 0 2.18-1.45 1.51-2.606L11.51 3.606a1.745 1.745 0 00-3.02 0L2.83 14.394C2.16 15.55 3 17 4.34 17z"
stroke="currentColor"
Expand All @@ -40,7 +30,13 @@ function EmptyWalletIcon() {

function CloseIcon() {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 1l8 8M9 1l-8 8"
stroke="currentColor"
Expand All @@ -58,6 +54,7 @@ function CloseIcon() {
*/
export function LowBalanceBanner(props: { thresholdUsd?: number }) {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const jwt = useAppSelector((state) => state.atomicAuth.jwt);
const mode = useAppSelector((state) => state.config.mode);
const balance = useAppSelector((state) => state.atomicAuth.balance);
Expand All @@ -76,25 +73,9 @@ export function LowBalanceBanner(props: { thresholdUsd?: number }) {
}
}, [jwt, mode, balance, dispatch]);

const handleTopUp = React.useCallback(async () => {
if (!jwt) return;
dispatch(atomicAuthActions.setTopupBusy(true));
dispatch(atomicAuthActions.setTopupError(null));
try {
const { checkoutUrl } = await atomicBackendApi.createPaygTopup(jwt, {
amountUsd: DEFAULT_TOPUP_USD,
successUrl: getStripePaygSuccessUrl(),
cancelUrl: STRIPE_PAYG_CANCEL_URL,
});
openExternal(checkoutUrl);
dispatch(atomicAuthActions.setTopupPending(true));
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
dispatch(atomicAuthActions.setTopupError(msg));
} finally {
dispatch(atomicAuthActions.setTopupBusy(false));
}
}, [jwt, dispatch]);
const handleTopUp = React.useCallback(() => {
navigate(routes.settings);
}, [navigate]);

const handleDismiss = React.useCallback(() => {
try {
Expand All @@ -119,13 +100,9 @@ export function LowBalanceBanner(props: { thresholdUsd?: number }) {
</div>
<div className={s.body}>
<div className={s.title}>No credits left</div>
<div className={s.subtitle}>Top up to continue using AI.</div>
<div className={s.subtitle}>Open settings to manage your balance.</div>
</div>
<button
type="button"
className={s.action}
onClick={() => void handleTopUp()}
>
<button type="button" className={s.action} onClick={handleTopUp}>
Top up
</button>
<button
Expand Down
Loading