Skip to content

[TV] Add the profile Settings sheet - #5794

Merged
sztomek merged 12 commits into
mainfrom
feat/tv-settings-sheet
Aug 28, 2026
Merged

[TV] Add the profile Settings sheet#5794
sztomek merged 12 commits into
mainfrom
feat/tv-settings-sheet

Conversation

@sztomek

@sztomek sztomek commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Description

Adds the Settings sheet to the Android TV profile modal, bringing it to parity with the Apple TV app (UI/Profile/SettingsMenuView.swift). The TV app previously had no settings surface of any kind.

Opening the profile modal now shows a Settings entry (in both the signed-in and signed-out states, matching tvOS). It presents a settings menu with four items:

  1. Subscription (signed-in only) — plan name (Plus/Patron + Monthly/Yearly), next renewal date (or Lifetime for Champion subscriptions), and a "manage on another platform" note for any subscription not purchased through Google Play (iOS/Web/Gift/Unknown). Free accounts show the free-account copy. No purchase/upsell flow (parity with tvOS).
  2. Privacy Policy → a QR-link screen (title, message, QR code of the support URL, and the URL as text).
  3. Terms of Use → the same QR-link screen for the ToS URL.
  4. Use episode artwork toggle → stored device-local in TvPreferences (the Kotlin equivalent of iOS Settings.loadEmbeddedImages, which the tvOS app deliberately keeps in local UserDefaults), default off. See the follow-up note below.

Reuses existing TV components (TvModal, TvModalButton, the qr module's rememberQrPainter) and the existing legal URL constants (Settings.INFO_PRIVACY_URL / Settings.INFO_TOS_URL).

Analytics

Fires the EventHorizon equivalents of the tvOS screen-view events, one-shot when each surface appears (all no-property):

  • SettingsGeneralShownEvent — settings menu appears
  • AccountDetailsSubscriptionEvent — subscription info appears
  • AccountDetailsShowPrivacyPolicyEvent — privacy QR appears
  • AccountDetailsShowTosEvent — terms QR appears
  • SettingsAppearanceUseEpisodeArtworkToggledEvent {enabled} — fired on every flip of the Use episode artwork toggle (matches the phone's appearance setter)
  • ProfileShownEvent — fired every time the profile modal opens (both signed-in and signed-out), matching tvOS ProfileMenuView.onAppear

All four events already exist in the generated EventHorizon catalog, so no schema PR is required.

Follow-up parity fixes (audit v2)

Four ride-along commits from the v2 parity audit (Part 9):

  1. Device-local artwork toggle (bug fix). The toggle previously wrote the synced Settings.artworkConfiguration with updateModifiedAt = true, so toggling it on the TV silently changed the user's phone setting. tvOS keeps this device-local. It now reads/writes TvPreferences instead. Known gap: no TV rendering path currently consumes this value — every TV episode row/cell renders podcast artwork (PodcastImage.getMediumArtworkUrl) regardless — so the toggle stores the preference but has no visible effect yet. Wiring TV episode-embedded artwork rendering is tracked as a separate follow-up (audit Part 5/8). This commit's purpose is to stop mutating the synced phone setting. (Because it now lives in the shared tv_preferences, it is also cleared on the TV sign-out data wipe, like the other TV device preferences.)
  2. Toggle analytics. Fire settings_appearance_use_episode_artwork_toggled {enabled} on every flip, matching the phone appearance setter.
  3. profile_shown. The profile modal previously tracked nothing. It now fires profile_shown on every open (signed-in and signed-out), matching tvOS ProfileMenuView.onAppear and the phone's ProfileViewModel.
  4. Other-platform note coverage. The "managed on another platform" note now shows for any non-Google-Play subscription (previously only iOS/Web; now also Gift/Unknown), matching iOS which shows it for any non-own-platform sub.

Fixes PCDROID-737 https://linear.app/a8c/issue/PCDROID-737/profile-settings-sheet

Testing Instructions

  1. On an Android TV device/emulator, open the profile modal (top-bar profile icon).
  2. Confirm a Settings entry appears (both signed-in and signed-out).
  3. Open Settings:
    • Signed in with a paid subscription: Subscription shows plan + next renewal; iOS/Web subs also show the other-platform note; a free account shows "Free Account".
    • Privacy Policy / Terms of Use: a QR code + the URL render, Back or Done dismisses.
    • Use episode artwork: toggling flips the checkmark and persists across app restarts. It is device-local (does not change the phone's setting) and, via LoggingAnalyticsListener logcat, fires settings_appearance_use_episode_artwork_toggled with the new enabled value.
    • Reopening the profile modal fires profile_shown each time (check logcat), signed-in or signed-out.
  4. Verify D-pad focus lands on a control in every screen and Back navigates out cleanly.

Screenshots or Screencast

Screen_recording_20260827_143023.mp4

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews
  • I have updated (or requested that someone edit) the Event Horizon schema to reflect any new or changed analytics. (No change needed — all four events already exist in the catalog.)

@dangermattic

dangermattic commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Reviewing PR #5794

  • Read AGENTS.md conventions
  • Read the PR diff
  • Read surrounding TV code (TvModal, TvScaffold, profile modal)
  • Verify subscription / settings APIs used
  • Post review

View job run · branch feat/tv-settings-sheet

@sztomek
sztomek force-pushed the feat/tv-settings-sheet branch from 9313b34 to b5b9032 Compare August 27, 2026 11:55
@sztomek
sztomek changed the base branch from main to feat/tv-listening-history August 27, 2026 11:55
@sztomek sztomek added this to the 8.20 milestone Aug 27, 2026
@sztomek sztomek added [Type] Feature Adding a new feature. [Area] TV labels Aug 27, 2026
@sztomek
sztomek marked this pull request as ready for review August 27, 2026 12:55
@sztomek
sztomek requested a review from a team as a code owner August 27, 2026 12:55
@sztomek
sztomek requested review from geekygecko and removed request for a team August 27, 2026 12:55
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 5m 16s —— View job


The structure is solid — the view model composes cleanly off Settings/SyncManager, the analytics events match existing usage in AccountDetailsFragment/PlaybackSettingsFragment, and the artwork write mirrors SettingsAppearanceViewModel exactly. One correctness issue: the subscription row presents expiryDate as a renewal date regardless of isAutoRenewing, which misinforms cancelled and gift subscribers. The rest is stacking/focus/localization polish.

Blocking

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsModal.kt:288 — "Next renewal" is shown unconditionally; branch on isAutoRenewing so cancelled subscriptions and non-Champion gifts get an expiry label instead of a renewal one, as the mobile header does. (thread)

Non-blocking

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/home/TvScaffold.kt:167 — unlike every sibling handler this leaves the profile modal open, so two TvModal dialogs stack and their dimAmount/FLAG_BLUR_BEHIND compound. (thread)
  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsModal.kt:131LaunchedEffect(isSignedIn) never re-runs after a sub-modal closes, so verify on a device that D-pad focus returns to the menu. (thread)

Nits

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsModal.kt:274"$tier $cycle" concatenates two translated words, baking in English word order; use a format string or two labels. (thread)
    · branch feat/tv-settings-sheet

Comment thread tv/src/main/java/au/com/shiftyjelly/pocketcasts/home/TvScaffold.kt Outdated
@sztomek
sztomek force-pushed the feat/tv-listening-history branch from b46fa11 to bee11b6 Compare August 27, 2026 13:18
@sztomek
sztomek force-pushed the feat/tv-settings-sheet branch from e142cb8 to 5c360df Compare August 27, 2026 13:23
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 1m 0s —— View job


Reviewed only 5c360df (the one commit since my last pass). The renewal-vs-expiry blocking finding is resolved: the label now branches on isAutoRenewing, and the date goes through toLocalizedFormatLongStyle(). No new findings in that commit.

Still open from the last review

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/home/TvScaffold.kt:171 — profile modal stays open behind the settings modal, so two dialogs' dim/blur compound. (thread)
  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsModal.kt:130 — menu focus is still keyed on isSignedIn only; worth confirming on a device that focus returns after backing out of a sub-modal. (thread)
  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsModal.kt:284 — (nit) tier + cycle still joined with a space. (thread)

Nits

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsModal.kt:288 — a Champion subscription takes the isChampion branch for both label and value, so the row reads "Next renewal: Lifetime". Consider giving Champion its own label (or no label) rather than pairing it with the renewal one.
    · branch feat/tv-settings-sheet

@sztomek
sztomek force-pushed the feat/tv-listening-history branch from bee11b6 to f4d55dc Compare August 27, 2026 19:33
@sztomek
sztomek force-pushed the feat/tv-settings-sheet branch from 5c360df to 3fac200 Compare August 27, 2026 19:47
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 2m 47s —— View job


Reviewed the three commits since my last pass (9753e04, 5ffbd47, 3fac200). The profile_shown tracking is correctly scoped to the modal's composition. The other two commits each introduce a user-facing correctness problem: moving the artwork toggle to TvPreferences disconnects it from anything that draws artwork, and widening the other-platform check to != Android makes it fire for gift and Champion subscriptions. My three earlier threads were all answered — not re-raising them.

Blocking

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsViewModel.kt:54 — the artwork pref is written and read only by this view model; TV episode rows always render podcast art (TvEpisodeRow.kt:141), so the toggle changes nothing and the analytics event no longer means what it means on mobile. (thread) · Fix this →
  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsModal.kt:251platform != Android now includes Gift/Unknown, so a Champion sees "Next renewal: Lifetime" plus "made on another platform, use that platform to manage it"; restrict to iOS/Web. (thread) · Fix this →

Non-blocking

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/preferences/TvPreferences.kt:36TvSignOutManager.kt:54 clears these prefs on sign-out, so the new device-local artwork preference is lost on log out. (thread)

Nits

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsModal.kt:290 — still open from the last pass: Champion takes the next_renewal label, so the row reads "Next renewal: Lifetime".
    · branch feat/tv-settings-sheet

)

fun setUseEpisodeArtwork(value: Boolean) {
tvPreferences.setUsingEpisodeArtwork(value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the toggle off settings.artworkConfiguration onto a TV-local pref makes it inert: nothing in tv/ ever calls TvPreferences.isUsingEpisodeArtwork() except this view model, and every episode row renders podcast art unconditionally (TvEpisodeRow.kt:141PodcastImage.getMediumArtworkUrl(podcastUuid); TvArtworkImage takes a plain model). So the user flips "Use episode artwork", the checkmark moves, an analytics event fires, and no artwork anywhere in the app changes.

Either wire the pref into the artwork models (episode art URL when enabled, podcast art otherwise) or drop the row until it does something. As-is it also poisons SettingsAppearanceUseEpisodeArtworkToggledEvent, which on mobile means the shared appearance setting actually changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by matching Apple TV. On tvOS the same toggle (SettingsMenuView) reads/writes the shared Settings.loadEmbeddedImages and is actually consumed by EpisodeRow (renders EpisodeArtworkView when on). We'd diverged twice: it was a throwaway TV-local pref, and nothing consumed it. Now: (1) the toggle binds to the shared settings.artworkConfiguration (same write as SettingsAppearanceViewModel), so the SettingsAppearanceUseEpisodeArtworkToggledEvent is honest again; (2) TvEpisodeRow loads episode artwork via PocketCastsImageRequestFactory when enabled, driven by a new LocalUseEpisodeArtwork provided from TvScaffold (same CompositionLocal pattern as LocalOpenNowPlaying). Removed the now-dead TvPreferences episode-artwork pref. Visual rendering still to be device-verified.

}

private val Subscription.isManagedOnAnotherPlatform: Boolean
get() = platform != SubscriptionPlatform.Android

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!= Android now also catches Gift and Unknown, which aren't purchases and have no platform to manage them on. Concretely, a Champion subscriber (isChampion requires platform == Gift, Subscription.kt:24) now sees "Next renewal: Lifetime" immediately followed by "This subscription was made on another platform. Please use that platform to manage the subscription." — self-contradictory, and there is no platform for them to go to.

Restrict this to the platforms the copy is about: platform == SubscriptionPlatform.iOS || platform == SubscriptionPlatform.Web, or at least exclude Gift.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — isManagedOnAnotherPlatform now checks platform == iOS || platform == Web instead of != Android, so Gift (Champion) and Unknown no longer trigger it. A Champion subscriber now sees "Next renewal: Lifetime" with no contradictory other-platform note.

return prefs.getBoolean(USE_EPISODE_ARTWORK_KEY, false)
}

fun setUsingEpisodeArtwork(isUsingEpisodeArtwork: Boolean) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(non-blocking) TvSignOutManager.kt:54 calls tvPreferences.clearAll(), so this display preference is wiped on sign-out along with the per-podcast archived state. The setting it replaced was account-scoped, so that was defensible; a device-local artwork preference arguably should survive logging out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved as a side effect of the artwork fix: the toggle no longer uses a TV-local pref at all (it writes the shared, synced settings.artworkConfiguration), so there's nothing display-related left in TvPreferences for clearAll() to wipe on sign-out.

@sztomek
sztomek force-pushed the feat/tv-listening-history branch from f4d55dc to edf5b84 Compare August 28, 2026 06:45
@sztomek
sztomek force-pushed the feat/tv-settings-sheet branch from 3fac200 to 2ec5362 Compare August 28, 2026 07:01
@sztomek
sztomek force-pushed the feat/tv-listening-history branch from edf5b84 to f48f27c Compare August 28, 2026 09:47
Base automatically changed from feat/tv-listening-history to main August 28, 2026 11:35
@sztomek
sztomek force-pushed the feat/tv-settings-sheet branch from 2ec5362 to 822daef Compare August 28, 2026 11:40
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 3m 11s —— View job


Reviewed 822daef, the only commit since my last pass. Both blocking findings are resolved: the toggle now writes the shared settings.artworkConfiguration with the same call as SettingsAppearanceViewModel.kt:123 and is consumed by TvEpisodeRow via LocalUseEpisodeArtwork, and isManagedOnAnotherPlatform is back to iOS/Web only. The dead TvPreferences pref is fully removed with no dangling references, and both view model tests were updated. Two loose ends around the new artwork plumbing, neither blocking.

Non-blocking

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/home/TvScaffoldViewModel.kt:54 — reads the master flag instead of ArtworkConfiguration.useEpisodeArtwork(element) like every other consumer, so a user who disabled artwork for e.g. Up Next on the phone still gets episode art in TV's Up Next; gate per screen with the matching Element. (thread)
  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/component/TvEpisodeRow.kt:148 — only this row honours the toggle, so a row shows episode art while its info modal, search result and now-playing screen show podcast art for the same episode; route those through the same local. (thread)

Nits

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/settings/TvSettingsModal.kt:287 — still open: isChampion takes the next_renewal label, so the row reads "Next renewal: Lifetime".
    · branch feat/tv-settings-sheet

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 49s —— View job


Reviewed f12bfcd, the only commit since my last pass. Both loose ends it targets are resolved: onSettings now closes the profile modal like every sibling handler, so the two TvModal dialogs no longer stack their dim/blur, and Champion subscriptions render "Lifetime" as a standalone line instead of pairing it with the "Next renewal" label — subscriptionRenewalLabel is now a clean isAutoRenewing branch and tv_settings_subscription_lifetime is still used. No new findings in this commit.

One behaviour change worth naming in the test pass: Back from Settings now returns to the home screen rather than to the profile modal, since the profile modal is dismissed on the way in.

Still open from the last review

  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/home/TvScaffoldViewModel.kt:54 — (non-blocking) still reads the master artwork flag instead of ArtworkConfiguration.useEpisodeArtwork(element), so a phone-side per-element opt-out is ignored on TV. (thread)
  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/component/TvEpisodeRow.kt:148 — (non-blocking) still the only TV surface honouring the toggle; the info modal, search row and now-playing screen show podcast art for the same episode. (thread)
    · branch feat/tv-settings-sheet

@sztomek
sztomek merged commit 0028e08 into main Aug 28, 2026
21 checks passed
@sztomek
sztomek deleted the feat/tv-settings-sheet branch August 28, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants