Stirling-Tools/Stirling-PDF · error · Error
No team resolved yet
Error message
No team resolved yet
What it means
Thrown by the `openPortal` callback in useWallet when the Stripe customer-portal session cannot be minted because `wallet.teamId` is null/undefined. The PAYG portal edge function (an RPC enforced outside Spring Security) requires a concrete teamId to resolve the caller's team and authorize the session, so refusing to call it without one prevents a guaranteed 400/403 from the backend.
Source
Thrown at frontend/editor/src/cloud/hooks/useWallet.ts:331
},
[devPreview, refetch],
);
const openPortal = useCallback(async () => {
if (devPreview) {
// No real Stripe in dev preview — open a placeholder so the click still
// feels alive. Routed through the openExternal seam to stay portable.
await openExternal("https://billing.stripe.com/p/login/mock");
return;
}
// Mint the portal session through the billing seam, passing teamId: the
// PAYG portal edge function needs it to resolve the caller's team outside
// Spring Security (its RPC enforces team membership). Then hand the URL to
// the openExternal seam so each platform routes it appropriately. The seam
// throws on error (e.g. 404 team_not_subscribed) so callers can toast.
const teamId = wallet?.teamId;
if (teamId == null) {
throw new Error("No team resolved yet");
}
const { url } = await createPortalSession({ teamId });
await openExternal(url);
}, [devPreview, wallet?.teamId]);
return {
wallet,
loading,
error,
refetch,
markSubscribed,
updateCap,
openPortal,
};
}
View on GitHub (pinned to 9ef20dcab8)
Solutions
- Gate the portal button on `wallet?.teamId` being present (and `loading===false`) so the click is only enabled once a team is resolved.
- Before throwing, call `refetch()` once and re-read teamId to recover from a stale cache.
- If this is a SaaS-only user with no PAYG team, hide the portal action entirely behind a feature check rather than letting the user click into an error.
- Toast a user-facing 'Your team is still being set up' message in the catch so the failure is not silent.
Example fix
// before
const openPortal = useCallback(async () => {
if (devPreview) { await openExternal("https://billing.stripe.com/p/login/mock"); return; }
const teamId = wallet?.teamId;
if (teamId == null) throw new Error("No team resolved yet");
const { url } = await createPortalSession({ teamId });
await openExternal(url);
}, [devPreview, wallet?.teamId]);
// after — gate the UI and refetch once before giving up
const openPortal = useCallback(async () => {
if (devPreview) { await openExternal("https://billing.stripe.com/p/login/mock"); return; }
let teamId = wallet?.teamId;
if (teamId == null) { await refetch(); teamId = walletRef.current?.teamId; }
if (teamId == null) throw new Error("No team resolved yet");
const { url } = await createPortalSession({ teamId });
await openExternal(url);
}, [devPreview, wallet?.teamId, refetch]); Defensive patterns
Strategy: validation
Validate before calling
// Before calling openPortal, confirm a team is resolvable
const canOpenPortal = !devPreview && wallet?.teamId != null && !loading;
if (!canOpenPortal) {
// optionally refetch once, then re-check
await refetch();
}
if (wallet?.teamId == null) {
// do not call openPortal; show 'team not ready' UI
} Type guard
function hasTeamId(w: { teamId?: string | null } | null | undefined): w is { teamId: string } {
return !!w && typeof w.teamId === "string" && w.teamId.length > 0;
} Try / catch
try {
await openPortal();
} catch (e) {
if (e instanceof Error && e.message === "No team resolved yet") {
toast.info("Your team is still being set up. Try again in a moment.");
await refetch();
} else {
toast.error("Could not open the billing portal.");
}
} Prevention
- Disable the 'Open portal' button until wallet?.teamId is non-null and loading is false.
- For SaaS-only users without a PAYG team, hide the portal action entirely instead of letting them click into an error.
- Refetch the wallet once before surfacing the error to recover from a stale cache.
When it happens
Trigger: Calling openPortal() before the wallet query has resolved (loading===true); the wallet endpoint returns a record with no teamId (user not yet on a team / not provisioned in PAYG); the wallet query errored and wallet is still its initial null state; devPreview is false but the user is in a non-PAYG (SaaS) flavor where teamId is never populated.
Common situations: User clicks the 'Manage billing / Open portal' button immediately on first wallet render; user belongs to a SaaS team that has no PAYG subscription so teamId is absent; race where refetch has not repopulated wallet after a team switch.
Related errors
- No team to invite to
- Member has no team to be removed from
- No current team
- No license key found. Please activate a license first.
- No valid license found. Please purchase a license before acc
AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13).
Data as JSON: /api/errors/2c8a8b77fa575154.
Report an issue: GitHub.