-
Notifications
You must be signed in to change notification settings - Fork 404
Fix Tabs component package inheritance for tabs without packages #5929
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
ac193d8
83fb53f
9dcfda8
a473d6f
8836ed0
02e70ba
8a6e4d8
8343596
0a8f5ae
441a44d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,20 +106,61 @@ struct LoadedTabsComponentView: View { | |
| defaultTabId: viewModel.defaultTabId | ||
| )) | ||
|
|
||
| // MARK: - Package Context Inheritance for Tabs | ||
| // | ||
| // This handles a nuanced scenario where tabs may or may not have their own packages: | ||
| // | ||
| // Example structure: | ||
| // - Package A (parent scope, default) | ||
| // - Package B (parent scope) | ||
| // - Tabs Component | ||
| // - Tab 1: has Package C (its own package) | ||
| // - Tab 2: no packages (should inherit from parent) | ||
| // | ||
| // Requirements: | ||
| // 1. Tabs WITH packages: use their own package context, propagate to parent for purchase button | ||
| // 2. Tabs WITHOUT packages: inherit from parent's selected package | ||
| // 3. Tabs WITHOUT packages should NOT be affected when tabs WITH packages propagate their package | ||
| // 4. Tabs WITHOUT packages SHOULD update when user selects a different parent package | ||
| // | ||
| // Solution: | ||
| // - Create a PackageContext for ALL tabs (not just those with packages) | ||
| // - Tabs with packages: initialized with their own packages | ||
| // - Tabs without packages: initialized with parent's current state, then kept in sync | ||
| // via `onChangeOf` observer that filters out tab propagations (see body) | ||
| // | ||
| self._tierPackageContexts = .init(initialValue: Dictionary( | ||
| uniqueKeysWithValues: viewModel.tabViewModels.map { key, tabViewModel in | ||
| let packageContext = PackageContext( | ||
| package: tabViewModel.defaultSelectedPackage, | ||
| variableContext: .init( | ||
| packages: tabViewModel.packages, | ||
| showZeroDecimalPlacePrices: parentPackageContext.variableContext.showZeroDecimalPlacePrices | ||
| uniqueKeysWithValues: viewModel.tabViewModels.map { key, tabViewModel -> (String, PackageContext) in | ||
| if !tabViewModel.packages.isEmpty { | ||
| // Tab has its own packages - create context with tab's packages | ||
| let packageContext = PackageContext( | ||
| package: tabViewModel.defaultSelectedPackage, | ||
| variableContext: .init( | ||
| packages: tabViewModel.packages, | ||
| showZeroDecimalPlacePrices: parentPackageContext.variableContext.showZeroDecimalPlacePrices | ||
| ) | ||
| ) | ||
| ) | ||
| return (key, packageContext) | ||
| return (key, packageContext) | ||
| } else { | ||
| // Tab has no packages - inherit from parent's current state. | ||
| // This context will be kept in sync with parent changes via `onChangeOf` | ||
| // in the body, which filters out propagations from tabs with packages. | ||
| let packageContext = PackageContext( | ||
| package: parentPackageContext.package, | ||
| variableContext: parentPackageContext.variableContext | ||
| ) | ||
|
||
| return (key, packageContext) | ||
| } | ||
| } | ||
| )) | ||
| } | ||
|
|
||
| /// Set of package identifiers that belong to tabs (not parent scope). | ||
| /// Used to distinguish between parent package selections and tab package propagations. | ||
| private var tabPackageIdentifiers: Set<String> { | ||
| Set(viewModel.tabViewModels.values.flatMap { $0.packages.map(\.identifier) }) | ||
| } | ||
|
|
||
| var body: some View { | ||
| if let activeTabViewModel, | ||
| let tierPackageContext = self.tierPackageContexts[self.tabControlContext.selectedTabId] { | ||
|
|
@@ -138,10 +179,8 @@ struct LoadedTabsComponentView: View { | |
| .onAppear { | ||
| if !wasConfigured { | ||
| self.wasConfigured = true | ||
| // In the event that the tabs components contain unique selected packages, we need to ensure that | ||
| // the first selected tab's selected package is propagated up to the purchase button. This sends | ||
| // that signal only for the initially rendered tab, then the onChange passed into the loadedTabView | ||
| // handles subsequent changes | ||
| // Propagate the initial tab's package to parent context for the purchase button. | ||
| // Subsequent changes are handled by the onChange callback in LoadedTabComponentView. | ||
| if let package = tierPackageContext.package { | ||
| self.packageContext.update( | ||
| package: package, | ||
|
|
@@ -150,6 +189,40 @@ struct LoadedTabsComponentView: View { | |
| } | ||
| } | ||
| } | ||
| .onChangeOf(self.packageContext.package) { newPackage in | ||
| self.syncParentPackageToTabsWithoutPackages(newPackage) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Sync parent package changes to tabs without packages | ||
| // | ||
| // This keeps tabs without packages in sync with parent package selections, | ||
| // while filtering out propagations from tabs that have their own packages. | ||
| // | ||
| // Flow example: | ||
| // 1. User selects Package B (parent) → packageContext updates to B | ||
| // 2. This function checks: is B a tab package? No. | ||
| // 3. Updates Tab 2's context (which has no packages) with Package B ✓ | ||
| // | ||
| // Filtered example: | ||
| // 1. Tab 1 propagates Package C → packageContext updates to C | ||
| // 2. This function checks: is C a tab package? Yes (it's in Tab 1). | ||
| // 3. Does NOT update Tab 2's context - Tab 2 keeps showing parent's package ✓ | ||
| // | ||
| private func syncParentPackageToTabsWithoutPackages(_ newPackage: Package?) { | ||
| guard let newPackage = newPackage else { return } | ||
|
|
||
| let isTabPackage = self.tabPackageIdentifiers.contains(newPackage.identifier) | ||
|
|
||
| if !isTabPackage { | ||
| // Parent package selection - update all tabs without packages | ||
| for (tabId, tabViewModel) in self.viewModel.tabViewModels where tabViewModel.packages.isEmpty { | ||
| self.tierPackageContexts[tabId]?.update( | ||
| package: newPackage, | ||
| variableContext: self.packageContext.variableContext | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading this I wonder what happens in this scenario, same example structure:
What package is shown in Tab 2?
According to the explanation below
This means that Tab 1 would always show the latest Parent package selected (A or B) even when Package C could be the latest package selected (from tab 2).
Is that what we want? That's what I gather from
But in the scenario above, what package is shown in Tab 2?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a video of the behavior:
Uploading Simulator Screen Recording - iPhone 16 Pro - 2025-12-12 at 10.38.25.mov…
For Tab2, the "active" package will be first the default in the parent scope, or the latest selected one.