Skip to content

Commit 9ae7c41

Browse files
sodiumsunclaude
andcommitted
power: full trial-to-paid journey in the Account card
The Power build IS the full app — when a trial ends the voice service just gates off (daemon _voice_gate_open keys on plan=='power') and narration keeps working; nothing is 'cut off' and there's no separate Pro app to download. Paying re-enables voice in place. _powerUpsell() renders one block with four states so the journey reads in one place: - trialing → 'Upgrade to Power … N days left' + monthly/annual - trial ended/Pro → 'back on Pro, add Power' + monthly/annual - trial ended/Free→ 'Power for voice, or Pro for cloud voices' + Pro option - never trialed → 'Try Power free' Needs a power_trial_used signal to tell 'ended' from 'never trialed'; synced from /v1/me via _sync_plan_from_me. All 9 states verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GTBZjBGurqsHUHsjaU1gA
1 parent 3db29f2 commit 9ae7c41

3 files changed

Lines changed: 43 additions & 3 deletions

File tree

heard/daemon.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,15 @@ def _sync_plan_from_me(self, me: dict) -> None:
12451245
changed = True
12461246
except Exception:
12471247
pass
1248+
server_used = me.get("power_trial_used")
1249+
if isinstance(server_used, bool) and server_used != bool(
1250+
self.cfg.get("power_trial_used")
1251+
):
1252+
try:
1253+
config.set_value("power_trial_used", server_used)
1254+
changed = True
1255+
except Exception:
1256+
pass
12481257
if changed:
12491258
_log("plan_synced_from_me", plan=server_plan)
12501259
self._reload_config()

heard/home_window.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ def _current_state() -> dict[str, Any]:
7373
# the opt-in Power trial. The OSS/Pro build never sets voice_service_cmd.
7474
"powerBuild": bool((cfg.get("voice_service_cmd") or "").strip()),
7575
"trialDaysLeft": trial_left,
76+
# Has this account ever used its one Power trial? Synced from /v1/me.
77+
# Distinguishes "trial ended → upgrade" from "never trialed → start free".
78+
"powerTrialUsed": bool(cfg.get("power_trial_used")),
7679
"onboardedPlan": cfg.get("onboarded_plan") or None,
7780
"agentConnected": _agent_connected(),
7881
"claudeConnected": _claude_connected(),

heard/onboarding.html

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,36 @@
256256
return [T.welcome, T.signin, T.connect, T.mode, T.voice, T.done]; // 2a Pro
257257
}
258258
function planWord(){ return S.plan==='power'?'Power':S.plan==='pro'?'Pro':'Free'; }
259+
// The Power upsell row in the Account card. One block, four states, so the
260+
// journey reads in one place: trialing → keep it; trial ended → upgrade (Pro
261+
// users get Power only, Free users get Power+Pro); never trialed → start free;
262+
// on Power plan but wrong build → get the app. Returns '' when nothing applies
263+
// (paying Power, or not signed in).
264+
function _powerUpsell(){
265+
const line = (t,s)=>`<div><div style="font-weight:600">${t}</div><div style="font:400 13px var(--sans);color:var(--ink-2);margin-top:2px">${s}</div></div>`;
266+
const payBtns = `<div style="display:flex;gap:8px;margin-top:12px"><button class="btn btn-primary" style="padding:9px 16px;flex:1" onclick="bridge('upgrade_power',{interval:'monthly'})">$30 / month</button><button class="btn btn-primary" style="padding:9px 16px;flex:1" onclick="bridge('upgrade_power',{interval:'annual'})">$24 / mo · annual</button></div>`;
267+
const wrap = (inner)=>`<div style="border-top:1px solid var(--line);margin:14px -18px 0;padding:14px 18px 0">${inner}</div>`;
268+
if(!S.signedIn) return '';
269+
// On Power plan but on the standard build → they need the Power app.
270+
if(S.plan==='power' && !S.powerBuild)
271+
return wrap(`<div style="display:flex;align-items:center;justify-content:space-between;gap:12px">${line('Get the Power app',"You're on Power - install the Power app to unlock voice input.")}<button class="btn btn-primary" style="padding:9px 16px" onclick="bridge('open_power_page')">Download</button></div>`);
272+
// Trialing → keep it.
273+
if(S.plan==='power' && S.trialDaysLeft!=null){
274+
const d = S.trialDaysLeft===0?'Your trial ends today':S.trialDaysLeft+' day'+(S.trialDaysLeft===1?'':'s')+' left';
275+
return wrap(line('Upgrade to Power', d+' in your trial - upgrade now to keep hands-free voice.')+payBtns);
276+
}
277+
if(!S.powerBuild) return ''; // OSS/Pro build, nothing Power to offer here
278+
// Trial ended (reverted off Power).
279+
if(S.plan!=='power' && S.powerTrialUsed){
280+
if(S.plan==='pro'||S.plan==='pro_plus')
281+
return wrap(line('Your Power trial ended',"You're back on Pro. Add Power for hands-free voice control.")+payBtns);
282+
return wrap(line('Your Power trial ended','Get Power for hands-free voice, or Pro for managed cloud voices.')+payBtns+`<button class="btn" style="padding:9px 16px;margin-top:8px;width:100%" onclick="bridge('manage_account')">Or upgrade to Pro - $15/mo</button>`);
283+
}
284+
// Never trialed, still eligible → start the free trial.
285+
if(S.plan!=='power' && !S.powerTrialUsed)
286+
return wrap(`<div style="display:flex;align-items:center;justify-content:space-between;gap:12px">${line('Try Power free','14 days of hands-free voice control. No card.')}<button class="btn btn-primary" style="padding:9px 16px" onclick="bridge('start_power_trial')">Start trial</button></div>`);
287+
return '';
288+
}
259289

260290
// ------------------------------------------------------------------ render
261291
let current = null;
@@ -481,9 +511,7 @@
481511
<div><div style="font-weight:600">Manage on heard.dev</div><div style="font:400 13px var(--sans);color:var(--ink-2);margin-top:2px">Plan, payment, and email in the browser.</div></div>
482512
<button class="btn btn-ghost" style="padding:9px 16px" onclick="bridge('manage_account')">Open</button>
483513
</div>
484-
${S.signedIn && S.plan!=='power' ? `<div style="border-top:1px solid var(--line);margin:14px -18px 0;padding:14px 18px 0;display:flex;align-items:center;justify-content:space-between;gap:12px"><div><div style="font-weight:600">Try Power free</div><div style="font:400 13px var(--sans);color:var(--ink-2);margin-top:2px">14 days of hands-free voice control. No card.</div></div><button class="btn btn-primary" style="padding:9px 16px" onclick="bridge('start_power_trial')">Start trial</button></div>` : ''}
485-
${S.plan==='power' && !S.powerBuild ? `<div style="border-top:1px solid var(--line);margin:14px -18px 0;padding:14px 18px 0;display:flex;align-items:center;justify-content:space-between;gap:12px"><div><div style="font-weight:600">Get the Power app</div><div style="font:400 13px var(--sans);color:var(--ink-2);margin-top:2px">You're on Power - install the Power app to unlock voice input.</div></div><button class="btn btn-primary" style="padding:9px 16px" onclick="bridge('open_power_page')">Download</button></div>` : ''}
486-
${S.plan==='power' && S.trialDaysLeft!=null ? `<div style="border-top:1px solid var(--line);margin:14px -18px 0;padding:14px 18px 0"><div style="display:flex;align-items:center;justify-content:space-between;gap:12px"><div><div style="font-weight:600">Keep Power</div><div style="font:400 13px var(--sans);color:var(--ink-2);margin-top:2px">${S.trialDaysLeft===0?'Your trial ends today':S.trialDaysLeft+' day'+(S.trialDaysLeft===1?'':'s')+' left in your trial'} - upgrade to keep hands-free voice.</div></div></div><div style="display:flex;gap:8px;margin-top:12px"><button class="btn btn-primary" style="padding:9px 16px;flex:1" onclick="bridge('upgrade_power',{interval:'monthly'})">$30 / month</button><button class="btn btn-primary" style="padding:9px 16px;flex:1" onclick="bridge('upgrade_power',{interval:'annual'})">$24 / mo · annual</button></div></div>` : ''}
514+
${_powerUpsell()}
487515
</div>
488516
${S.byokEnabled ? `${_HD('API keys')}
489517
<div style="font:400 12.5px/1.5 var(--sans);color:var(--ink-2);margin:-6px 0 10px">Bring your own keys. Heard sends narration <b>straight to these providers</b> using your key - never through our servers. Leave blank to use your plan’s cloud voices, or the local Kokoro voice for zero cloud.</div>

0 commit comments

Comments
 (0)