Skip to content

Move part of WhatsNew to module#4129

Draft
mokagio wants to merge 4 commits into
trunkfrom
ainfra-2185-make-whats-newinto-a-real-module
Draft

Move part of WhatsNew to module#4129
mokagio wants to merge 4 commits into
trunkfrom
ainfra-2185-make-whats-newinto-a-real-module

Conversation

@mokagio

@mokagio mokagio commented Apr 14, 2026

Copy link
Copy Markdown
Contributor
📘 Part of: AINFRA-2185

Follows the EndOfYear precedent from #4106: extract the non-app-coupled parts of podcasts/Whats New/ into a new WhatsNew SPM target.

Commits (read in order)

  1. Move part of WhatsNew to module — three pure-SwiftUI animation views (AutoplayWhatsNewHeader, GiveRatingsWhatsNewHeader, ClipsWhatsNewView) + their six imagesets as SPM resources, plus the Announcement value type.
  2. Rename class WhatsNew to WhatsNewPresenter — removes the module/class name shadow.
  3. Nest Announcement inside WhatsNew namespaceWhatsNewAnnouncementWhatsNew.Announcement via a caseless public enum WhatsNew.
  4. Extract visibleAnnouncement picker to module — pure selection logic + majorMinor/inRange helpers move to WhatsNew.visibleAnnouncement(from:...). Presenter keeps the Settings.lastWhatsNewShown side-effect and becomes a thin wrapper.

Scope

Moves: three animation views, WhatsNew.Announcement, the picker, version helpers, WhatsNew.normalizedVersion(_:), six imagesets (whatsnew_autoplay, whatsnew_star, whatsnew_clip_preview, four whatsnew_clips_*).

Stays in app: WhatsNewPresenter, WhatsNewView, WhatsNewFullView, Announcements, BookmarksAnnouncement, SlumberViews, DeselectChaptersAnnouncementViewModel, and the 25 remaining imagesets (folder/homegrid/plus/bookmarks variants). All depend on Theme/L10n/Analytics/Settings/NavigationManager/PaidFeature, which is the rabbit hole this PR explicitly avoids.

Module deps: PocketCastsServer (SubscriptionTier), PocketCastsUtils (Color.init(hex:), Collection.subscript(safe:)).

To test

The thing most likely to silently break is the asset-catalog wiring — Image("whatsnew_autoplay") compiles fine but renders empty if bundle: .module is missing or the imageset isn't in Resources/Media.xcassets. Steps (1)–(3) cover that end-to-end.

1. SwiftUI previews — primary visual check, no simulator needed

Each moved view still has its PreviewProvider / #Preview. Open in Xcode and confirm the canvas renders the asset:

  • Modules/Sources/WhatsNew/AutoplayWhatsNewHeader.swift — spinning autoplay icon on a blue gradient circle.
  • Modules/Sources/WhatsNew/GiveRatingsWhatsNewHeader.swift — five stars doing the bounce animation.
  • Modules/Sources/WhatsNew/ClipsWhatsNewView.swiftno #Preview on trunk; drop a temporary #Preview { ClipsWhatsNewView() } at the bottom if you want to eyeball it, or rely on step (2).

The previews go through the exact same Image("name", bundle: .module) code path as production, so a broken asset wiring shows up immediately as a missing-image glyph.

2. Asset-catalog sanity check — strongest autonomous signal

After any make build_staging:

xcrun --sdk iphonesimulator assetutil --info \
  ~/Library/Developer/Xcode/DerivedData/podcasts-*/Build/Products/StagingDebug-iphonesimulator/podcasts.app/Modules_WhatsNew.bundle/Assets.car \
  | grep '"Name"' | sort -u

Expected (plus ZZZZPackedAsset-* bookkeeping entries, which are fine):

  • whatsnew_autoplay
  • whatsnew_clip_preview
  • whatsnew_clips_instagram
  • whatsnew_clips_telegram
  • whatsnew_clips_tumblr
  • whatsnew_clips_whatsapp
  • whatsnew_star

If Modules_WhatsNew.bundle/Assets.car doesn't exist, the resources: [.process("Resources")] declaration isn't taking effect. If an imageset is missing, a git mv left it behind.

3. Unit tests
make test_staging ONLY_TESTING='PocketCastsTests/WhatsNewtests'

All 14 cases should pass — exercises the presenter through the extracted picker and catches any regression in selection logic or the value-type migration.

4. Full presenter flow in the simulator — optional, requires editing Announcements.swift

There is no Debug menu hook; the presenter just reads UserDefaults. And there's a wrinkle: the naive flow (poke LastRunVersion, launch) does not visually verify any of the moved views.

Current app version on trunk is 8.9 (see config/Version.xcconfig), so every announcement is within range. The picker returns announcements.last(where: ...) — the last enabled entry in array order. With default feature flags:

Announcement Version Default enabled? Uses a moved view?
Autoplay 7.43 AutoplayWhatsNewHeader
Slumber 7.57 ❌ (slumber flag off)
Deselect Chapters ×3 7.60 / 7.61 depends on tier
Give Ratings 7.70 GiveRatingsWhatsNewHeader
Clips 7.72 ClipsWhatsNewView
Up Next Shuffle 7.77 ✅ (upNextShuffle flag on) UpNextAnnouncementView, unchanged

So by default you get Up Next Shuffle, which this PR doesn't touch. To exercise a moved header, temporarily comment out later entries in podcasts/Whats New/Announcements.swift:

  • Clips → delete the Up Next Shuffle entry.
  • Give Ratings → delete Up Next Shuffle + Clips.
  • Autoplay → delete everything after Autoplay.

Then:

xcrun simctl spawn booted defaults write au.com.shiftyjelly.pocketcasts.staging LastRunVersion 7.42
xcrun simctl spawn booted defaults delete au.com.shiftyjelly.pocketcasts.staging LastWhatsNewShown

(Verify the bundle id against the Staging scheme.)

The moved animation headers render inside WhatsNewFullView / WhatsNewView (app-side, unchanged), so if the full banner looks right, the moved subview is fine.

What I verified locally

  • make build_staging — green after each of the four commits.
  • PocketCastsTests/WhatsNewtests — 14/14 passing after the initial extraction and again after the picker extraction.
  • xcrun assetutil --info on Modules_WhatsNew.bundle/Assets.car — all seven expected imagesets present.
  • Not done: rendering the banners in the running simulator. The SwiftUI previews in step (1) are the closest I got.

Checklist

  • Release notes in CHANGELOG.md. (No — internal refactor, no behavior change.)
  • Unit tests for the change. (Existing WhatsNewtests exercise the presenter through the extracted picker.)
  • Analytics spreadsheet updated. (No analytics changed.)

🤖 Generated with the help of Claude Code

Follows the `EndOfYear` precedent (#4106): extract the pieces of
`podcasts/Whats New/` that carry no app-target coupling into a new
`WhatsNew` SPM target, shrinking the app target and giving module-level
consumers a typed API to work with.

This move covers the three pure-SwiftUI animation views and the
`Announcement` value type:

- `AutoplayWhatsNewHeader`
- `GiveRatingsWhatsNewHeader`
- `ClipsWhatsNewView` (plus its fileprivate `AnimatedLogoImageView`)
- The nested `WhatsNew.Announcement` struct, promoted to a top-level
  `public struct WhatsNewAnnouncement`.
  The app-side `class WhatsNew` keeps a
  `typealias Announcement = WhatsNewAnnouncement` so every existing
  call site (`[WhatsNew.Announcement]`, `.init(...)`, etc.) compiles
  unchanged.

Six imagesets used only by the moved views come along as SPM bundle
resources (`whatsnew_autoplay`, `whatsnew_star`, `whatsnew_clip_preview`,
four `whatsnew_clips_*`). All references go through
`Image("name", bundle: .module)`. The remaining `podcasts/WhatsNew.xcassets`
entries (bookmarks, folder, homegrid, plus variants) stay in the app
target because their consumers — `BookmarksAnnouncement`, `SlumberViews`,
etc. — still depend on `Theme`, `L10n`, `NavigationManager`, `PaidFeature`
and other app-only primitives. Pulling those across would spiral into
extracting half the app target, so they're explicitly out of scope here.

Dependencies of the new `WhatsNew` target: `PocketCastsServer` (for
`SubscriptionTier` used by `WhatsNewAnnouncement.displayTier`) and
`PocketCastsUtils` (for `Color.init(hex:)` used by
`AutoplayWhatsNewHeader`'s gradient).

Verified locally:

- `make build_staging` — green after each individual move (five
  intermediate builds) and after the final import scrub.
- `WhatsNewtests` — 14 passing, confirming the typealias keeps the
  class-side API stable.
- `assetutil --info` on the built `Modules_WhatsNew.bundle/Assets.car`
  reports all seven moved imagesets.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dangermattic

Copy link
Copy Markdown
Collaborator
2 Warnings
⚠️ Modules/Package.swift was changed without updating its corresponding Package.resolved. Please resolve the Swift packages in Xcode.
⚠️ View files have been modified, but no screenshot or video is included in the pull request. Consider adding some for clarity.
1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

@pocketcasts

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in Pocket Casts Prototype Build by scanning the QR code below to install the corresponding build.
App NamePocket Casts Prototype Build
Build Number14124
VersionPR #4129
Bundle IDau.com.shiftyjelly.podcasts.prototype
Commit9bd83e7
Installation URL5714a1onu7hso
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

mokagio and others added 3 commits April 14, 2026 14:42
The `WhatsNew` SPM module name was shadowed by `class WhatsNew` in the
app target. That forced the Announcement value type to carry a
`WhatsNewAnnouncement` name at the module level plus a typealias inside
the class, because Swift has no `import Module as Alias` syntax to
reach a module-qualified type when a same-named type shadows it.

Renaming the class removes the shadow so later commits can collapse
`WhatsNewAnnouncement` into `WhatsNew.Announcement` via a caseless
`enum WhatsNew` namespace in the module, and eventually extract the
visible-announcement picker without fighting name resolution.

This commit is pure rename. The typealias is dropped and every call
site (`podcasts` + tests + `WhatsNewView`/`WhatsNewFullView`, which
previously reached the type implicitly through the typealias) now
imports `WhatsNew` and references `WhatsNewAnnouncement` directly —
that ugliness is temporary and gets undone in the next commit.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Now that `class WhatsNew` is gone, define a caseless `public enum
WhatsNew` in the module and nest `Announcement` inside it. Callers
can write `WhatsNew.Announcement` again, matching the pre-module
source shape without the `WhatsNewAnnouncement` stopgap from the
previous commit.

The enum acts purely as a namespace. Future picker extraction and
anything else that belongs on the module side can nest inside the
same `WhatsNew` enum.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves the pure selection logic — "given a list of announcements and
a version state, return the one to show" — out of
`WhatsNewPresenter` and into `WhatsNew.visibleAnnouncement(from:...)`
on the module side. The version helpers (`majorMinor`, `inRange`)
go with it as file-internal `String` extensions, and the module
exposes `WhatsNew.normalizedVersion(_:)` so the presenter can keep
normalizing versions in its init without touching `String.majorMinor`
directly.

The presenter's `visibleAnnouncement` computed property becomes a
thin wrapper that performs the first-run side-effect
(`Settings.lastWhatsNewShown = currentVersion`) and then delegates
to the module's pure picker. That's the same logic as before, just
split along the app/module seam: the UIKit+Settings side-effect
stays in the app, the pure selection stays in the module where it's
testable without any app-target dependency.

Verified with `make build_staging` and `PocketCastsTests/WhatsNewtests`
(14/14 passing) after each move.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants