Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
86879e6
unbounded: phase 1 — tab shell + Unbounded as a top-level tab
May 12, 2026
3ed84c3
unbounded: phase 2 — Unbounded Settings sheet + hide-tab toggle
May 12, 2026
11abe38
unbounded: phase 3 — auto-enable on VPN connect
May 12, 2026
e191f63
unbounded: phase 4 — first-visit welcome popup + info bubble
May 12, 2026
dcf42b7
unbounded: also auto-enable on app launch (not just VPN connect)
May 12, 2026
76f59cb
unbounded: lift the heart spray out of the pill, onto the globe
May 12, 2026
ec18d7f
unbounded: put the Lottie inside the pill, overflowing — matches CSS
May 12, 2026
1dfee34
unbounded: match the pill exactly — heart+text, bottom-left anchor
May 12, 2026
74a1d4b
unbounded: revert the pill back to centered
May 12, 2026
8ad21a5
unbounded: persist "Total people helped to date" across restarts
May 12, 2026
6b4cf04
share: auto-fall-back from SmC to Unbounded on any Start failure
May 13, 2026
d856624
unbounded: gate entire UI surface on server Features[unbounded] flag
May 13, 2026
f028351
share: render Lottie arrival heart-burst at native canvas size
May 14, 2026
caba92d
share: nudge heart-to-text gap from 10 → 14 px
May 14, 2026
74f58f0
smc: address Copilot review on #8820
myleshorton May 31, 2026
9024906
review: i18n the Unbounded UI + preserve totalCount on idle reset
myleshorton Jun 2, 2026
de0b5b6
review: preserve totalCount on Start error, remove dead Lottie layer,…
myleshorton Jun 2, 2026
c9fe5fa
share_my_connection: clarify peer-arrival persist invariant
myleshorton Jun 2, 2026
53c2d22
share_my_connection: roll back state if Unbounded fallback also fails
myleshorton Jun 2, 2026
b8557fe
review: honor unboundedHidden in auto-enable, drop microtask, fix act…
myleshorton Jun 2, 2026
bd61905
deps: bump radiance for the unbounded connection-source fix
myleshorton Jun 7, 2026
0bfd915
unbounded: fix globe origin — call geo /lookup + use precise coords
myleshorton Jun 7, 2026
12aca41
unbounded: route peer geo lookups through Lantern's own service
myleshorton Jun 7, 2026
e4a005e
unbounded: stop the globe burning CPU off-tab and after toggle-off
myleshorton Jun 7, 2026
c491c67
unbounded: address review — globe visibility via tab animation, dialo…
myleshorton Jun 7, 2026
8fe94b8
unbounded: make the globe static — rotate-to-arc instead of spinning
myleshorton Jun 8, 2026
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
73 changes: 62 additions & 11 deletions assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ msgstr "Share Referral Code"
msgid "unbounded"
msgstr "Unbounded"

# Tab label shown alongside the Unbounded tab on the Home screen when
# the dual-tab strip is active.
msgid "vpn"
msgstr "VPN"

msgid "help_fight_global_internet_censorship"
msgstr "Help Fight Global Internet Censorship"

Expand Down Expand Up @@ -557,12 +562,15 @@ msgstr "Share My Connection"
msgid "share_my_connection_subtitle"
msgstr "Let other Lantern users route through your connection to bypass censorship."

msgid "share_my_connection_on_tap_to_view"
msgstr "On — tap to view"

# Share My Connection screen — body / hero copy
# Unbounded tab — hero copy. Short variant for the tab embed; the
# longer privacy explanation now lives in the welcome dialog
# (showUnboundedWelcomeDialog) so the tab body stays scannable.
msgid "smc_intro"
msgstr "Help others bypass censorship by sharing a small portion of your home internet connection. While sharing is on, traffic from users in censored regions will egress through your IP."
msgstr "Help others bypass censorship by securely sharing your connection."

# Info-bubble tooltip on the Unbounded tab header
msgid "about_unbounded"
msgstr "About Unbounded"
Comment thread
myleshorton marked this conversation as resolved.

# Status card — phase labels
msgid "smc_status_label"
Expand Down Expand Up @@ -607,19 +615,44 @@ msgstr "Couldn't share: %s"
msgid "smc_status_error_generic"
msgstr "Couldn't share — try toggling again"

# Status card — stats + tooltip
# Status card — stats + tooltip. Counts the number of remote
# clients currently routing through THIS device (people being
# helped, not helpers); parallel framing to smc_stat_total_helped
# (same semantics, lifetime).
msgid "smc_stat_active_now"
msgstr "Active now"
msgstr "People being helped right now"

msgid "smc_stat_total_today"
msgstr "Total this session"
# Lifetime ("to date") rather than daily — backed by the persisted
# unboundedTotalHelped setting so the count survives app restarts.
msgid "smc_stat_total_helped"
msgstr "Total people helped to date"

msgid "smc_connections_tooltip"
msgstr "Most connections are short liveness probes — Lantern clients periodically check that this peer is reachable before sending real traffic. A quick burst from many locations is normal; an arc that lingers represents an actual user session."

# Arrival toast — "New connection from {country}"
# Arrival toast surfaced when a censored user starts routing through
# this peer. The "Helping a new person" framing is intentional —
# emphasizes the user-impact framing the SmC tab leans into.
msgid "smc_arrival_toast"
msgstr "New connection from %s"
msgstr "Helping a new person in %s"

# Pill shown beneath the globe when Unbounded is active but no peer is
# currently routing through this device.
msgid "unbounded_waiting_for_connections"
msgstr "Waiting for connections..."

# Welcome dialog shown the first time the Unbounded tab opens.
msgid "unbounded_welcome_title"
msgstr "Welcome to Unbounded"

msgid "unbounded_welcome_body_1"
msgstr "When you enable Unbounded, your device becomes part of a network of 'digital bridges' to the open internet. Censored users connect to these bridges, allowing them to bypass government-imposed restrictions and access the information they need."

msgid "unbounded_welcome_body_2"
msgstr "This collective effort makes censorship harder to enforce, expanding access to the open internet."

msgid "unbounded_welcome_body_3"
msgstr "You can remove Unbounded from the interface anytime in Settings."

# Advanced section / manual port forward
msgid "smc_advanced"
Expand Down Expand Up @@ -674,6 +707,24 @@ msgstr "Basic mode (Unbounded)"
msgid "smc_disclosure_full"
msgstr "Full mode (SmC)"

# Unbounded Settings menu entry (Settings → Unbounded Settings)
msgid "unbounded_settings_title"
msgstr "Unbounded Settings"

# Auto-enable Unbounded toggle
msgid "auto_enable_unbounded"
msgstr "Auto-enable Unbounded"

msgid "auto_enable_unbounded_subtitle"
msgstr "Turn on automatically when Lantern is open"

# Hide Unbounded toggle (collapses the Unbounded tab on the Home shell)
msgid "hide_unbounded"
msgstr "Hide Unbounded"

msgid "hide_unbounded_subtitle"
msgstr "Removes Unbounded from the top of this screen"

msgid "vpn_connected"
msgstr "Lantern is now connected."

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ replace github.com/quic-go/qpack => github.com/quic-go/qpack v0.5.1
require (
github.com/alecthomas/assert/v2 v2.3.0
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9
github.com/getlantern/radiance v0.0.0-20260531221356-11aa55a6ff16
github.com/getlantern/radiance v0.0.0-20260607000056-0a9e276a9e00
github.com/sagernet/sing-box v1.12.22
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0
golang.org/x/sys v0.41.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YL
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q=
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b h1:gMYJzEhLrmIqQ+JnjiYNm+UyUDalK3WUmVyecFwmV5g=
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b/go.mod h1:NpfXdK4ldEKkjQ4P1R+DBF4ua5VFOlxmgHROTnYrApg=
github.com/getlantern/radiance v0.0.0-20260531221356-11aa55a6ff16 h1:CpsYjT3sBimvg/GNYO5IKvRjWDc4BCeDjDQxUNsx8gA=
github.com/getlantern/radiance v0.0.0-20260531221356-11aa55a6ff16/go.mod h1:wemClXaug4hwPdsUEm8g1bCa8tkjk3UjDM+6PfWJwMI=
github.com/getlantern/radiance v0.0.0-20260607000056-0a9e276a9e00 h1:+KshI14A9S7fpfjWGFynGjUarD30oukj0vhhjoQ2YpU=
github.com/getlantern/radiance v0.0.0-20260607000056-0a9e276a9e00/go.mod h1:wemClXaug4hwPdsUEm8g1bCa8tkjk3UjDM+6PfWJwMI=
github.com/getlantern/samizdat v0.0.3-0.20260529191731-5ea8ae61ddbf h1:KxiMF+oG0rTtuBi7GiIaHfccYOf69rLJ/VnO5myoYc4=
github.com/getlantern/samizdat v0.0.3-0.20260529191731-5ea8ae61ddbf/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/semconv v0.0.0-20260327040646-21845dda05cb h1:c5YM7b3a4r2J8Eh89KkI6M/iTFe6Bi+b8AJlfkKdFq4=
Expand Down
38 changes: 38 additions & 0 deletions lib/core/models/app_setting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ class AppSetting {
final bool successfulConnection;
final String dataCapThreshold;
final bool onboardingCompleted;
// Unbounded preferences. autoEnable: turn the peer share on whenever
// the VPN connects (defaults on per the Figma spec). hideTab: hide
// the Unbounded tab + collapse the tab bar when the user doesn't
// want to see it. welcomeSeen: tracks the first-visit info popup so
// we only show it once. All persisted across launches.
final bool unboundedAutoEnable;
final bool unboundedHidden;
final bool unboundedWelcomeSeen;
// Lifetime running total of peers this device has helped. Survives
// restarts so the "Total people helped to date" stat in the
// Unbounded tab can keep climbing — that's the spec wording in the
// Figma. ShareNotifier seeds totalCount from this on build, and
// writes back each time the count increments.
final int unboundedTotalHelped;

const AppSetting({
this.themeMode = 'system',
Expand All @@ -19,6 +33,10 @@ class AppSetting {
this.successfulConnection = false,
this.dataCapThreshold = '',
this.onboardingCompleted = false,
this.unboundedAutoEnable = true,
this.unboundedHidden = false,
this.unboundedWelcomeSeen = false,
this.unboundedTotalHelped = 0,
});

AppSetting copyWith({
Expand All @@ -31,6 +49,10 @@ class AppSetting {
bool? successfulConnection,
String? dataCapThreshold,
bool? onboardingCompleted,
bool? unboundedAutoEnable,
bool? unboundedHidden,
bool? unboundedWelcomeSeen,
int? unboundedTotalHelped,
}) {
return AppSetting(
locale: newLocale ?? locale,
Expand All @@ -42,6 +64,11 @@ class AppSetting {
successfulConnection: successfulConnection ?? this.successfulConnection,
dataCapThreshold: dataCapThreshold ?? this.dataCapThreshold,
onboardingCompleted: onboardingCompleted ?? this.onboardingCompleted,
unboundedAutoEnable: unboundedAutoEnable ?? this.unboundedAutoEnable,
unboundedHidden: unboundedHidden ?? this.unboundedHidden,
unboundedWelcomeSeen: unboundedWelcomeSeen ?? this.unboundedWelcomeSeen,
unboundedTotalHelped:
unboundedTotalHelped ?? this.unboundedTotalHelped,
);
}

Expand All @@ -55,6 +82,10 @@ class AppSetting {
'successfulConnection': successfulConnection,
'dataCapThreshold': dataCapThreshold,
'onboardingCompleted': onboardingCompleted,
'unboundedAutoEnable': unboundedAutoEnable,
'unboundedHidden': unboundedHidden,
'unboundedWelcomeSeen': unboundedWelcomeSeen,
'unboundedTotalHelped': unboundedTotalHelped,
};

factory AppSetting.fromJson(Map<String, dynamic> json) => AppSetting(
Expand All @@ -67,5 +98,12 @@ class AppSetting {
successfulConnection: json['successfulConnection'] == true,
dataCapThreshold: (json['dataCapThreshold'] ?? '').toString(),
onboardingCompleted: json['onboardingCompleted'] == true,
// Default to true when missing (first-time post-upgrade users
// should get the auto-enable behaviour the spec calls for).
unboundedAutoEnable: json['unboundedAutoEnable'] != false,
unboundedHidden: json['unboundedHidden'] == true,
unboundedWelcomeSeen: json['unboundedWelcomeSeen'] == true,
unboundedTotalHelped:
(json['unboundedTotalHelped'] as num?)?.toInt() ?? 0,
);
}
10 changes: 9 additions & 1 deletion lib/core/models/feature_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ enum FeatureFlag {
metrics('otel.metrics'),
traces('otel.traces'),
autoUpdateEnabled('autoUpdateEnabled'),
androidSideloadAutoUpdateEnabled('androidSideloadAutoUpdateEnabled');
androidSideloadAutoUpdateEnabled('androidSideloadAutoUpdateEnabled'),
// Server-side gate for the entire Unbounded / Share My Connection
// surface. When false (the default for censored regions), the
// Unbounded tab, settings entry, project link, and auto-enable hooks
// all disappear — censored users should never see a "share your
// connection" UI that could draw attention to them on-device. Mirrors
// radiance/unbounded/unbounded.go shouldRunUnbounded, which already
// gates execution on the same Features[UNBOUNDED] flag.
unbounded('unbounded');

final String key;

Expand Down
4 changes: 4 additions & 0 deletions lib/core/router/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class AppRouter extends RootStackRouter {
path: '/vpn-setting',
page: VPNSetting.page,
),
AutoRoute(
path: '/unbounded-setting',
page: UnboundedSetting.page,
),
AutoRoute(
path: '/account',
page: Account.page,
Expand Down
Loading
Loading