Referral UI updates#8916
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughReferral data is added to user models and local storage, with derived bonus totals. The invite page gains download-link and QR-code actions, while home displays conversion rewards. Account, settings, and plan messaging update referral presentation and store-version visibility. ChangesReferral and invite flow
Radiance dependency update
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Home
participant homeProvider
participant checkAndShowReferralReward
participant LocalStorageService
participant ReferralRewardDialog
participant SharePlus
homeProvider->>Home: user update
Home->>checkAndShowReferralReward: schedule post-frame check
checkAndShowReferralReward->>LocalStorageService: load seen referral IDs
checkAndShowReferralReward->>LocalStorageService: save converted referral IDs
checkAndShowReferralReward->>ReferralRewardDialog: show reward dialog
ReferralRewardDialog->>SharePlus: share referral message
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR adds a referral rewards experience to Lantern, spanning user-data modeling, persistent “seen” tracking for one-time reward notifications, and UI updates to surface earned referral months and share download links.
Changes:
- Introduces referral data modeling (
ReferralModel) and computed referral bonus helpers (referralBonusDays/referralBonusMonths). - Adds one-time “referral converted” reward dialog logic backed by local storage state.
- Updates Account + Invite Friends UI and adds direct download links (plus QR rendering dependency) to support referral sharing.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pubspec.yaml | Adds qr_flutter dependency for QR rendering. |
| pubspec.lock | Locks transitive qr + qr_flutter versions. |
| lib/features/setting/setting.dart | Hides the “Get 30 days of Pro free” tile in store builds. |
| lib/features/setting/referral_reward_dialog.dart | Implements converted-referral reward dialog and “seen” gating. |
| lib/features/setting/invite_friends.dart | Refreshes Invite Friends UI, adds earned-months display and download-links sheet with QR. |
| lib/features/home/home.dart | Hooks into user refreshes to trigger referral reward dialog post-frame. |
| lib/features/account/account.dart | Displays earned referral months as a plan subtitle. |
| lib/core/services/local_storage_service.dart | Adds persistence for “seen converted referrals”. |
| lib/core/models/user.dart | Adds ReferralModel and includes it in UserDataModel JSON parsing/serialization. |
| lib/core/extensions/user_data.dart | Adds referral bonus day/month computed helpers. |
| lib/core/common/app_urls.dart | Adds direct download URLs for Android/Windows/macOS installers. |
| assets/locales/en.po | Adds referral-related localized strings used by the updated UI/dialog. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/features/setting/invite_friends.dart (1)
112-120: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winGuard against setting a disposed
ValueNotifierafter the copy delay.Both
_onCopyTapimplementationsawait Future.delayed(...)then mutateisCopied.value. If the widget/sheet is disposed before the delay elapses, this throwsA ValueNotifier<bool> was used after being disposed.in debug/profile builds. UseuseIsMounted()fromflutter_hooksto guard the post-delay write.🔒 Proposed fix using useIsMounted
- Future<void> _onCopyTap( - ValueNotifier<bool> isCopied, - String referralCode, - ) async { + Future<void> _onCopyTap( + ValueNotifier<bool> isCopied, + String referralCode, + bool Function() isMounted, + ) async { copyToClipboard(referralCode); isCopied.value = true; await Future.delayed(Duration(seconds: 1)); - isCopied.value = false; + if (isMounted()) isCopied.value = false; }Also applies to: 356-364
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/features/setting/invite_friends.dart` around lines 112 - 120, Update both _onCopyTap implementations to obtain the flutter_hooks useIsMounted guard and check it after the one-second delay before assigning isCopied.value = false; keep the immediate copied-state update and clipboard behavior unchanged.
🧹 Nitpick comments (1)
lib/features/setting/invite_friends.dart (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated "split
%sinto boldTextSpans" logic across two new files. Both_reminderCalloutand_messageSpansindependently reimplement the same fragile pattern of manually splitting a localized string on the literal'%s'to interleave bold spans.
lib/features/setting/invite_friends.dart#L318-354: extract a shared helper (e.g.buildRichTextWithPlaceholders(template, values, boldStyle)) that both this and_messageSpanscan use, rather than duplicating the split logic.lib/features/setting/referral_reward_dialog.dart#L112-130: switch_messageSpansto use the same shared helper once extracted.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/features/setting/invite_friends.dart` at line 1, Extract the duplicated placeholder-splitting and bold-span construction from _reminderCallout and _messageSpans into a shared helper, such as buildRichTextWithPlaceholders, preserving the existing template, values, and boldStyle behavior. Update both methods in invite_friends.dart and referral_reward_dialog.dart to call the helper instead of manually splitting localized strings on '%s'.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/core/models/user.dart`:
- Around line 82-109: Update ReferralModel.fromJson so the converted field
evaluates with a strict true check rather than preserving arbitrary JSON types;
use the converted == true pattern so null, strings, numbers, and other
unexpected values become false while actual true remains true.
In `@lib/features/setting/invite_friends.dart`:
- Line 21: Update the referral flow around the bonusMonths calculation to use
the existing referralBonusMonths value instead of legacyUserData.bonusMonths,
keeping the displayed total consistent with other referral surfaces. Fix the
months_earned label to use singular wording when the value is 1 and plural
wording otherwise.
In `@lib/features/setting/referral_reward_dialog.dart`:
- Around line 18-51: Update checkAndShowReferralReward and
_showReferralRewardDialog to preserve exactly-once behavior: persist the union
of previously seen IDs and all currently converted referral IDs, reserve the
dialog-visible state before any await to prevent overlapping calls, and reset
_rewardDialogVisible whenever the dialog future completes, including
outside/back dismissal rather than only button callbacks.
---
Outside diff comments:
In `@lib/features/setting/invite_friends.dart`:
- Around line 112-120: Update both _onCopyTap implementations to obtain the
flutter_hooks useIsMounted guard and check it after the one-second delay before
assigning isCopied.value = false; keep the immediate copied-state update and
clipboard behavior unchanged.
---
Nitpick comments:
In `@lib/features/setting/invite_friends.dart`:
- Line 1: Extract the duplicated placeholder-splitting and bold-span
construction from _reminderCallout and _messageSpans into a shared helper, such
as buildRichTextWithPlaceholders, preserving the existing template, values, and
boldStyle behavior. Update both methods in invite_friends.dart and
referral_reward_dialog.dart to call the helper instead of manually splitting
localized strings on '%s'.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2b6b06bb-2f18-41f2-a7ba-3b2b5349c85f
⛔ Files ignored due to path filters (1)
pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
assets/locales/en.polib/core/common/app_urls.dartlib/core/extensions/user_data.dartlib/core/models/user.dartlib/core/services/local_storage_service.dartlib/features/account/account.dartlib/features/home/home.dartlib/features/setting/invite_friends.dartlib/features/setting/referral_reward_dialog.dartlib/features/setting/setting.dartpubspec.yaml
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/core/common/app_dialog.dart (1)
175-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake callers explicitly handle the returned Future.
lib/features/account/account.dart:404still callscustomDialogfrom a synchronous method withoutawaitorunawaited. Make that caller async and await the dialog, or explicitly useunawaited(...)if fire-and-forget behavior is intentional.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/core/common/app_dialog.dart` around lines 175 - 181, Update the caller of customDialog in the account flow to explicitly handle its returned Future: make the surrounding method asynchronous and await customDialog, or use unawaited(customDialog(...)) if the dialog should remain fire-and-forget. Ensure the chosen handling preserves the intended behavior and satisfies async error propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@lib/core/common/app_dialog.dart`:
- Around line 175-181: Update the caller of customDialog in the account flow to
explicitly handle its returned Future: make the surrounding method asynchronous
and await customDialog, or use unawaited(customDialog(...)) if the dialog should
remain fire-and-forget. Ensure the chosen handling preserves the intended
behavior and satisfies async error propagation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ee185b79-5f62-4ece-a769-24675e10d9ae
📒 Files selected for processing (6)
assets/locales/en.polib/core/common/app_dialog.dartlib/core/models/user.dartlib/features/account/account.dartlib/features/setting/invite_friends.dartlib/features/setting/referral_reward_dialog.dart
🚧 Files skipped from review as they are similar to previous changes (5)
- assets/locales/en.po
- lib/core/models/user.dart
- lib/features/setting/referral_reward_dialog.dart
- lib/features/setting/invite_friends.dart
- lib/features/account/account.dart
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 15 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
lib/features/setting/referral_reward_dialog.dart:37
checkAndShowReferralRewardpersists converted referral IDs as "seen" before confirming the dialog can be shown. If the widget is disposed/unmounted before the dialog appears (or if showing the dialog throws), the referral can be permanently marked seen and the user will never be notified. Also,_rewardDialogVisibleisn't guarded by atry/finally, so an exception could leave it stucktrue.
_rewardDialogVisible = true;
// Mark as seen before showing so the dialog can't repeat. Merge with the
// already-seen ids so referrals missing from this response stay seen.
await storage.saveSeenConvertedReferrals(
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 15 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
lib/features/setting/referral_reward_dialog.dart:37
- Seen referral IDs are persisted before verifying
context.mountedand before the dialog is actually shown. If the widget unmounts (or the dialog throws) after saving, the user may never see the reward dialog, and_rewardDialogVisiblecan remain stucktrueif an exception occurs. Consider only persisting after the dialog successfully shows, and usetry/finallyto always reset the visibility flag.
_rewardDialogVisible = true;
// Mark as seen before showing so the dialog can't repeat. Merge with the
// already-seen ids so referrals missing from this response stay seen.
await storage.saveSeenConvertedReferrals(
lib/features/setting/invite_friends.dart:30
BaseScreendoes not provide scrolling (it inserts thebodydirectly into aScaffold), but this screen now uses a non-scrollableColumnwith aSpacer(). On smaller devices / large text settings this is likely to overflow vertically with no way to scroll to the buttons. Consider restoring aSingleChildScrollViewor restructuring asColumn(children: [Expanded(SingleChildScrollView(...)), buttons]).
return BaseScreen(
title: 'invite_friends'.i18n,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 19 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (4)
lib/features/setting/referral_reward_dialog.dart:39
- The reward dialog is marked visible and referrals are persisted as "seen" before confirming the dialog can actually be shown. If the widget unmounts (or an exception occurs) before the dialog is displayed, the user can permanently miss the notification, and
_rewardDialogVisiblemay never reset. Consider usingtry/finallyto always reset the flag, checkingcontext.mountedbefore showing, and persisting "seen" only after the dialog is successfully shown/dismissed. Also avoid forcingnewMonthsto at least 1 ifbonusDaysEarnedis unexpectedly < 30 to keep behavior consistent withreferralBonusMonths.
_rewardDialogVisible = true;
// Mark as seen before showing so the dialog can't repeat. Merge with the
// already-seen ids so referrals missing from this response stay seen.
await storage.saveSeenConvertedReferrals(
{...seen, ...converted.map((r) => r.userId)}.toList(),
);
lib/features/auth/choose_payment_method.dart:540
getReferralMessage()is localized, but the UI removes the English word "free" viareplaceAll('free', ''). In non-English locales this won’t strip anything, so you can end up displaying the localized word for “free” twice (once in the message and once in the right column), and it can also leave trailing whitespace in English. Prefer rendering the full localized message as-is and drop the separate "free" column (or introduce a dedicated i18n key that does not include the word “free”).
Text(
getReferralMessage().replaceAll('free', '').toTitleCase(),
style: theme.bodyMedium,
),
Text(
'free'.i18n,
style: theme.bodyMedium!.copyWith(
color: context.textDisabled,
),
),
lib/features/setting/invite_friends.dart:33
BaseScreendoesn’t provide scrolling, and this screen’sbodyis a plainColumnwith fixed bottom buttons. With longer localized strings and/or smaller screens (e.g., landscape), this can overflow vertically. Consider making the content scrollable (e.g.,SingleChildScrollView, orCustomScrollView/SliverFillRemainingto keep buttons reachable) to avoid render overflow.
return BaseScreen(
title: 'invite_friends'.i18n,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
assets/locales/en.po:1285
referral_message_2yuses singular “month” but should be plural for 2 months.
msgid "referral_message_1m"
msgstr "+ 1 additional month free"
msgid "referral_message_1y"
msgstr "+ 1 additional month free"
msgid "referral_message_2y"
msgstr "+ 2 additional month free"
* Fix Android user data startup race (#8915) * Fix Android user data startup race * code review updates * code review updates * Bump radiance for non-blocking fronted-config startup (#8920) Bumps radiance to b72b27d, carrying the fronted-config init fix into the client. domainfront (=> 2862f7a) moves with it as an indirect dep. radiance no longer fetches the fronting config (fronted.yaml.gz) from raw.githubusercontent.com synchronously on the init critical path — a fetch that stalled startup for 30s per cold start where GitHub is blocked (China), contributing to the Android "Pro users shown as logged-out" cluster (getlantern/engineering#3716, symptom fixed in #8915). domainfront now owns the config lifecycle (fetch off the critical path → persist → bootstrap from the persisted copy), so startup does no blocking network I/O. go.mod / go.sum only; ran `go mod tidy`. `go build ./...` passes. Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Provide a writable WebView2 user-data folder on Windows (#8921) * Provide webview path for Windows. * go mod tidy: drop stray go-arg/go-scalar from go.sum These entries were added to go.sum with no corresponding go.mod require and nothing imports them, so go mod tidy removes them. Orphaned go.sum entries can cause Go tooling (notably gomobile) to resolve transitive deps to older hashed versions, so keep go.sum consistent with go.mod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * windows/webview: bail on unexpanded path or failed folder creation Address review on EnsureWebView2UserDataFolder: - If %LOCALAPPDATA% (or any referenced var) is undefined, ExpandEnvironmentStringsW leaves it literal in the output; bail rather than create a garbage relative folder and set WEBVIEW2_USER_DATA_FOLDER to an unusable value. - Only set WEBVIEW2_USER_DATA_FOLDER after confirming the target exists as a directory (via GetFileAttributesW); if CreateDirectoryW failed, leave WebView2 to its default instead of pointing it at a nonexistent folder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * windows/webview: narrow unexpanded-path guard to leading '%' '%' is legal elsewhere in a Windows folder name (e.g. a username), so bailing on any '%' could reject a valid expanded path. An undefined %LOCALAPPDATA% leaves the template literal, so the result starts with '%' — check only the leading char. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * windows/webview: treat empty WEBVIEW2_USER_DATA_FOLDER as unset The null-buffer size probe returns >0 for a set-but-empty variable, so an empty override would be respected and leave WebView2 with an unusable folder. Read into a buffer instead: GetEnvironmentVariableW returns 0 when the var is unset or empty, so we only bail on a real non-empty override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: jigar-f <jigar@getlantern.org> Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Bump radiance for multi-URL config racing (#8924) Bumps radiance to 838849783d37 (kindling + domainfront move with it), carrying the variadic WithConfigURL / multi-URL config racing capability (getlantern/domainfront#17) into the client. No behavior change yet: the client still fetches from the single GitHub config URL; the China-reachable mirror URL will be added to the raced set once it's publishing (getlantern/engineering#3721). go.mod / go.sum only; ran go mod tidy; go build ./... passes. Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Fix referral reward UI edge cases * code review changes --------- Co-authored-by: Myles Horton <afisk@getlantern.org> Co-authored-by: Adam Fisk <a@lantern.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: jigar-f <jigar@getlantern.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
lib/features/setting/referral_reward_dialog.dart:45
_showReferralRewardDialog(...)is not awaited. That makes_rewardDialogVisiblereset (and referral IDs persist) before the dialog is dismissed, which can allow multiple dialogs to stack and defeats the “persist only after dismiss” behavior described in the comment.
_showReferralRewardDialog(
context: context,
newMonths: newMonths,
totalMonths: totalMonths,
referralCode: user.legacyUserData.referral.toUpperCase(),
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
lib/features/setting/referral_reward_dialog.dart:45
- The reward dialog is not awaited, so
saveSeenConvertedReferralsruns immediately and can mark referrals as “seen” even if the dialog fails to show or before the user dismisses it. This contradicts the comment above and makes it easier to miss notifications.
_showReferralRewardDialog(
context: context,
newMonths: newMonths,
totalMonths: totalMonths,
referralCode: user.legacyUserData.referral.toUpperCase(),
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
lib/features/setting/referral_reward_dialog.dart:55
- _showReferralRewardDialog() isn't awaited, so seen-referrals are persisted immediately (despite the comment) and _rewardDialogVisible is reset before the dialog is dismissed. This can lead to the reward dialog re-triggering on subsequent user refreshes and/or referrals being marked seen even if the dialog fails to display.
_showReferralRewardDialog(
context: context,
newMonths: newMonths,
totalMonths: totalMonths,
referralCode: user.legacyUserData.referral.toUpperCase(),
);
// Persist only after the user dismisses the dialog. If the route cannot be
// shown or the app exits first, the notification is retried next time.
await storage.saveSeenConvertedReferrals(
{...seen, ...converted.map((r) => r.userId)}.toList(),
);
This pull request introduces a comprehensive referral rewards system, including backend model changes, UI updates, and local storage enhancements. The main focus is to allow users to earn and track free months of Lantern Pro when their referrals convert, and to display this information in the app. The changes span localization, data models, persistent storage, and user interface components.
Referral rewards system implementation:
ReferralModeltouser.dartand integrated it intoUserDataModel, including serialization/deserialization logic for storing and retrieving referral data. [1] [2] [3] [4] [5]user_data.dartto compute total referral bonus days and months earned, making it easy to display and use this information throughout the app.Localization and UI updates:
en.pofor referral program messaging, including descriptions, reward notifications, and download link instructions.Persistent storage and reward dialog:
LocalStorageServiceto track which converted referrals have already triggered the reward dialog, ensuring users are notified only once per referral. [1] [2]home.dartto listen for new converted referrals and show a congratulatory dialog when appropriate. [1] [2]Direct download links:
app_urls.dart, to facilitate sharing with friends during the referral process.Summary by CodeRabbit
New Features
Improvements
Localization