Skip to content

Commit e232ef0

Browse files
sodiumsunclaude
andcommitted
power: auto-start the trial on sign-in (Power build only)
The Power build now auto-enrolls a fresh sign-in into the 14-day trial by re-calling the existing _maybe_start_power_trial (it was written then left uncalled when the flow went opt-in). Possessing the Power build means the user already passed the gated download, so signing in IS the opt-in. Safe on every sign-in: the server no-ops if already Power and refuses if the one trial was used (power_trial_used_at). A paying Pro user is preserved via base_plan and reverts to Pro at expiry. OSS builds skip it (no voice_service_cmd). The 'Try Power free' button stays as a fallback for an offline-at-sign-in miss. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GTBZjBGurqsHUHsjaU1gA
1 parent ba00125 commit e232ef0

2 files changed

Lines changed: 110 additions & 4 deletions

File tree

heard/url_scheme.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,14 @@ def _apply_token(token: str, plan: str, email: str, trial_expires_at: int) -> No
163163
except Exception:
164164
pass
165165
config.set_value("heard_trial_expires_at", int(trial_expires_at or 0))
166-
# NOTE: the Power trial is now OPT-IN — the user clicks "Start Power trial"
167-
# on the Power-build welcome (home_window._act_start_power_trial), which
168-
# calls /v1/power/trial/start. We no longer auto-enroll on sign-in, so a Pro
169-
# user on the Power build keeps their plan until they explicitly opt in.
166+
# Power build: auto-start the 14-day trial on sign-in. Possessing the Power
167+
# build already means they came through the gated download, so signing in IS
168+
# the opt-in. Safe on every sign-in — the server no-ops if they're already
169+
# Power and refuses if the one trial was used (power_trial_used_at). A paying
170+
# Pro user is preserved via base_plan and reverts to Pro at expiry, not Free.
171+
# OSS builds skip it (no voice_service_cmd). The "Try Power free" button
172+
# remains as a fallback if this call fails (offline at sign-in).
173+
_maybe_start_power_trial(token)
170174
_reload_and_selftest()
171175
_bring_onboarding_forward_signed_in(email or "your account")
172176
# Also refresh the persistent Heard window (the new WebView home/onboarding)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Auto-start the Power trial on sign-in — but ONLY on the Power build.
2+
3+
Possessing the Power build means the user came through the gated download, so
4+
signing in is the opt-in. The server endpoint is idempotent and one-trial-per-
5+
account, so calling it on every sign-in is safe; these tests pin the CLIENT
6+
side: it fires for a Power build and never for an OSS build.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import json
12+
from types import SimpleNamespace
13+
14+
import pytest
15+
16+
from heard import url_scheme
17+
18+
19+
class _FakeResp:
20+
def __init__(self, payload):
21+
self._p = json.dumps(payload).encode()
22+
23+
def read(self):
24+
return self._p
25+
26+
def __enter__(self):
27+
return self
28+
29+
def __exit__(self, *a):
30+
return False
31+
32+
33+
@pytest.fixture
34+
def cfg(monkeypatch):
35+
store = {"heard_api_base": "https://api.heard.dev"}
36+
monkeypatch.setattr(url_scheme.config, "load", lambda: dict(store))
37+
monkeypatch.setattr(url_scheme.config, "set_value",
38+
lambda k, v: store.__setitem__(k, v))
39+
return store
40+
41+
42+
def _capture_requests(monkeypatch, payload):
43+
calls = []
44+
45+
def fake_urlopen(req, *a, **kw):
46+
calls.append(req.full_url)
47+
return _FakeResp(payload)
48+
49+
monkeypatch.setattr(url_scheme.urllib.request, "urlopen", fake_urlopen)
50+
return calls
51+
52+
53+
def test_oss_build_never_starts_a_trial(cfg, monkeypatch):
54+
cfg["voice_service_cmd"] = "" # OSS build
55+
calls = _capture_requests(monkeypatch, {"plan": "power"})
56+
url_scheme._maybe_start_power_trial("tok")
57+
assert calls == [], "OSS build must not call the Power trial endpoint"
58+
assert cfg.get("heard_plan") is None
59+
60+
61+
def test_power_build_starts_the_trial_and_persists_plan(cfg, monkeypatch):
62+
cfg["voice_service_cmd"] = "python -m heard_power serve" # Power build
63+
calls = _capture_requests(
64+
monkeypatch, {"plan": "power", "trial_expires_at": 1234567890}
65+
)
66+
url_scheme._maybe_start_power_trial("tok")
67+
assert any("/v1/power/trial/start" in u for u in calls)
68+
assert cfg["heard_plan"] == "power"
69+
assert cfg["heard_trial_expires_at"] == 1234567890
70+
71+
72+
def test_trial_already_used_does_not_flip_local_plan(cfg, monkeypatch):
73+
"""Server refuses (trial_used) → we must not claim Power locally."""
74+
cfg["voice_service_cmd"] = "python -m heard_power serve"
75+
_capture_requests(monkeypatch, {"ok": False, "reason": "trial_used", "plan": "expired"})
76+
url_scheme._maybe_start_power_trial("tok")
77+
assert cfg.get("heard_plan") != "power"
78+
79+
80+
def test_network_failure_is_swallowed(cfg, monkeypatch):
81+
cfg["voice_service_cmd"] = "python -m heard_power serve"
82+
83+
def boom(*a, **kw):
84+
raise OSError("offline")
85+
86+
monkeypatch.setattr(url_scheme.urllib.request, "urlopen", boom)
87+
url_scheme._maybe_start_power_trial("tok") # must not raise
88+
assert cfg.get("heard_plan") != "power"
89+
90+
91+
def test_apply_token_invokes_autostart(cfg, monkeypatch):
92+
"""The sign-in path must actually call the autostart hook."""
93+
cfg["voice_service_cmd"] = "python -m heard_power serve"
94+
seen = {}
95+
monkeypatch.setattr(url_scheme, "_maybe_start_power_trial",
96+
lambda tok: seen.setdefault("tok", tok))
97+
monkeypatch.setattr(url_scheme, "_refresh_byok_enabled", lambda tok: None)
98+
monkeypatch.setattr(url_scheme, "_reload_and_selftest", lambda: None)
99+
monkeypatch.setattr(url_scheme, "_bring_onboarding_forward_signed_in", lambda e: None)
100+
101+
url_scheme._apply_token("tok123", "trial", "", 0)
102+
assert seen.get("tok") == "tok123", "sign-in did not auto-start the Power trial"

0 commit comments

Comments
 (0)