Skip to content

[MOB-3199] Default Browser Settings Card #889

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

Merged
merged 40 commits into from
Apr 15, 2025

Conversation

d4r1091
Copy link
Member

@d4r1091 d4r1091 commented Apr 10, 2025

MOB-3199

Context

We wanted to improve the User's discovery of how to set the Ecosia mobile app as the default browser.
Upon upgrading, all users will see this new card appear, which will take them to a page detail showing how to set Ecosia as their default browser.

Approach

ℹ️ Some of this code contains documentation like the EcosiaText as being a completely isolated component. Some don't. It's the first concrete approach to make our codebase rely almost completely on our self-contained components from the Ecosia Framework. Happy to review namings and layouts if needed.

The inspiration for rewriting SwiftUI a reusable component (the configurable NTP card we developed a while ago) was that the two designs were similar in the high-level structure.
The card has been reviewed matching this new SwiftUI configurable component.

We wanted to use Lottie (which Firefox already implements) and we had to make some small adjustments in order to make the animation aligned with the expected design 🎨 .

The focus here wasn’t just shipping a new card, but building an isolated, reusable system that:

  • Doesn’t rely on Firefox’s global theming infrastructure (at least of the reusable components, not their wrappers like the HeaderView)
  • Supports Markdown localization out-of-the-box (thanks SwiftUI 🙌 )

Everything from layout to styling was scoped specifically for Ecosia (see EcosiaText below).

🧱 SwiftUI + UIKit Integration

  • ConfigurableNudgeCardView is a standalone SwiftUI view used in both UICollectionViewCell and UITableViewHeaderFooterView, without needing UIKit layout or dependencies.

  • UIKit still handles screen flow and navigation — SwiftUI is used where it shines: layout, styling, and interaction.

  • The new DefaultBrowserCoordinator (potentially utilizable for the NudgeCard show/hide too 🤷 ) bridges between UIKit navigation and SwiftUI instruction views, keeping presentation logic clean.

  • 🔌 No dependency on Firefox theming:
    All new components (cards, instruction steps, etc.) define their own Style structs (e.g. NudgeCardStyle, InstructionStepsViewStyle) and accept them as parameters.
    This avoids relying on shared theme singletons or assumptions from the broader Firefox app.

  • 🧳 Self-contained views:
    Each SwiftUI component (e.g. EcosiaText, InstructionStepsView, ConfigurableNudgeCardView) can be previewed, tested, and used independently. Most of them rely on their own "style" (e.g. NudgeCardStyle, InstructionStepsViewStyle).

  • 🗣️ Markdown Localization Support

This one has been quite challenging as we wanted to get the best of both, all matched with our own enum based localized string.

  • Introduced EcosiaText, a SwiftUI wrapper that reads from .strings and parses Markdown using AttributedString(markdown:).
  • All localized strings now support inline formatting like **bold**, no extra styling logic is required.
  • This removes the need for custom NSAttributedString construction in code as we also rely on our own String.swift enum.

🧪 Testing

Introduced https://github.com/nalexn/ViewInspector to better write tests for SwiftUI

🧹 Cleanup & Removal

  • Removed the old static default browser cell from settings.
  • All related logic lies into Ecosia/UI/Settings/DefaultBrowser/

Checklist

  • I performed some relevant testing on a real device and/or simulator
  • I updated only the english localization source files if needed
  • I added the // Ecosia: helper comments where needed
  • I made sure that any change to the Analytics events included in PR won't alter current analytics (e.g. new users, upgrading users)

@d4r1091 d4r1091 requested a review from a team April 10, 2025 12:09
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Typo

The function name "isDefautlBrowserCell" contains a typo which may reduce code clarity and consistency. Consider renaming it to "isDefaultBrowserCell" to avoid confusion.

func isDefautlBrowserCell(_ section: Int) -> Bool {
    settings[section].children.first?.accessibilityIdentifier == "DefaultBrowserSettings"
}

Flag Logic
The implementation of the show/hide methods for the default browser nudge card uses inverted boolean values. Please verify that the logic is intentional and documented, as it can be confusing for maintainers.

Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Safely unwrap settings URL

Safely unwrap the URL to prevent a potential crash if the URL conversion fails.

firefox-ios/Client/Ecosia/UI/Settings/DefaultBrowser/DefaultBrowserCoordinator.swift [34]

-UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:])
+if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
+    UIApplication.shared.open(settingsURL, options: [:])
+}
Suggestion importance[1-10]: 7

__

Why: Introducing safe unwrapping prevents a potential crash when converting the settings URL, making the code more robust.

Medium
General
Correct function name typo

Rename the misspelled function name to improve readability and avoid confusion.

firefox-ios/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift [181-183]

-func isDefautlBrowserCell(_ section: Int) -> Bool {
+func isDefaultBrowserCell(_ section: Int) -> Bool {
     settings[section].children.first?.accessibilityIdentifier == "DefaultBrowserSettings"
 }
Suggestion importance[1-10]: 4

__

Why: The suggestion fixes a simple typo in the function name, improving readability without affecting functionality.

Low

@d4r1091
Copy link
Member Author

d4r1091 commented Apr 10, 2025

Same issue with the failing linter. We can tackle this separately

Copy link
Collaborator

@lucaschifino lucaschifino left a comment

Choose a reason for hiding this comment

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

Looks great! Awesome to be pushing for more SwiftUI ❤️

I left some questions, I think the only major one would be a discussion about the different approaches to applying themes in SwiftUI, let me know what you think 🙂


public static func makeDefaultCoordinatorAndShowDetailViewFrom(_ navigationController: UINavigationController?,
analyticsLabel: Analytics.Label.DefaultBrowser,
topViewContentBackground: Color,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you elaborate why topViewContentBackground is a configurable parameter? Seems like we always have the same color, so I guess it could be part of the view's styling?

This also introduces a new color snowflake hardcoding EcosiaColor.DarkGreen50.color, but I guess that part is okay if it's a conscious decision and specific case.

Copy link
Member Author

@d4r1091 d4r1091 Apr 14, 2025

Choose a reason for hiding this comment

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

Of course! Let me explain the main reason:

The InstructionStepsView and its InstructionStepsViewStyle aim to be as much theme-agnostic as possible.
The EcosiaColor.DarkGreen50.color valued with 0x275243 doesn't have any dark/light correspondence (hence becoming a snowflake ❄️ ).
The EcosiaColor.DarkGreen50.color can't be accessed via the Ecosia framework as it lies under the Client/Ecosia folder.
There is a DefaultBrowserCoordinator that contains a handy factory function to make the InstructionStepsViewStyle and inject it into its showDetailView(::) function.
This coordinator also lies under the Ecosia framework and cannot access the EcosiaColor constants.
We could pass the style as parameter of the DefaultBrowserCoordinator.makeDefaultCoordinatorAndShowDetailViewFrom(:::) but then we would need to create the style twice (one for the EcosiaSettings and one for the AppSettingsTableViewController+Ecosia).
This function makes it more straightforward to make the style once and pass the topViewContentBackground and its theme (as the coordinator not being a SwiftUI.View but only a navigation pattern that we can bin 🗑️ whenever we want, keeping the UI InstructionStepsView theme agnostic).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks! Agreed, we can keep it.

This coordinator also lies under the Ecosia framework and cannot access the EcosiaColor constants.

This one issue I think we'll need to fix in the future, but no need now since there are other reasons behind it 🙏

override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = super.tableView(
tableView,
viewForHeaderInSection: section
) as! ThemedTableSectionHeaderFooterView
return headerView
}
*/
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if shouldShowDefaultBrowserNudgeCardInSection(section),
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will always be the first section, won't it? Any reason for having the shouldShowDefaultBrowserNudgeCardInSection instead of hardcoding?

You'd still need to check the User object to see if it should be shown, but I wonder why we need the accessibilityIdentifier logic.

Copy link
Member Author

@d4r1091 d4r1091 Apr 14, 2025

Choose a reason for hiding this comment

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

Mmmmh good point. The suggestion would basically check whether we are at the first section (and the card should be shown) instead of a more complex check via the accessibilityIdentifier?
To better elaborate it, I chose to check whether:

  • it is the first section
  • the accessibility identifier is "default browser"

...only to make sure we are adding the card in the correct place, as if the card shouldn't be shown, there won't be a Default Browser section either

Comment on lines 73 to +75
func applyTheme(theme: Theme) {
mainContainerStackView.backgroundColor = theme.colors.ecosia.backgroundSecondary
closeButton.tintColor = theme.colors.ecosia.iconDecorative
titleLabel.textColor = theme.colors.ecosia.textPrimary
descriptionLabel.textColor = theme.colors.ecosia.textSecondary
actionButton.setTitleColor(theme.colors.ecosia.buttonBackgroundPrimary, for: .normal)
guard let viewModel else { return }
configure(with: viewModel, theme: theme)
Copy link
Collaborator

Choose a reason for hiding this comment

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

🔌 No dependency on Firefox theming:
All new components (cards, instruction steps, etc.) define their own Style structs (e.g. NudgeCardStyle, InstructionStepsViewStyle) and accept them as parameters.
This avoids relying on shared theme singletons or assumptions from the broader Firefox app.

🧳 Self-contained views:
Each SwiftUI component (e.g. EcosiaText, InstructionStepsView, ConfigurableNudgeCardView) can be previewed, tested, and used independently. Most of them rely on their own "style" (e.g. NudgeCardStyle, InstructionStepsViewStyle).

I noticed all SwiftUI views receive all of the colors on their init and believe the reasoning is explained on the PR description (quote above).

Even so, I think this has two negative consequences:

  1. When theme changes, we have to manually recreate the whole view and change the root instead of just updating colors like seen on this code here. This is a simple view and not a big problem, but I think for more complex ones this pattern would be an issue.
  2. The views themselves don't know about their own colors and all of the styling is extracted to the parent hosting view controller (like in this file or on the new coordinator). I think this in the end is harder to read and maintain compared to leaving the responsibility to the views to know their own colors and react to theme changes.

I think those are solved when using @Environment(\.themeManager) var themeManager and ThemeDidChange notifications like Firefox does and like we did for the SeedCounterView.

I understand the benefits you mentioned of "This avoids relying on shared theme singletons or assumptions from the broader Firefox app." and "Each SwiftUI component (e.g. EcosiaText, InstructionStepsView, ConfigurableNudgeCardView) can be previewed, tested, and used independently", but can't we still achieve those using the same theme manager pattern?

Copy link
Member Author

Choose a reason for hiding this comment

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

Love these kinds of discussions on updates and indeed I should have clarified the intentions behind it 🙏 .
As it's SwiftUI, the inner body of its struct gets re-valued every time we try to mutate any of the view's content (e.g. any of its inner view's backgrounds). So it kind of "recreates" the view but I agree that performance-wise it's not the same as destroying and rebuilding it in memory. The reason why I chose this one this time in contrast to the SeedCounterView (which will still be changed anytime soon in favor of any newer UI) is that this view gets accessed from an inner section of our App which isn't the NTP.
The lifetime of these views is likely less than a view that always stays on the NTP.
For this specific scenario, where the SwiftUI view is wired into the hierarchy via UIKit, we are re-assigning the rootView of a "sticky" card.
I wouldn't worry much about performance, considering that we will only have one sticky on the NTP.
We could further optimize it, making the viewModel of that NTPConfigurableNudgeCardCell conform to equatable but to the previous point, considering it's just one (and the view itself doesn't hold any specific "state") I don't see the issue. Happy to discuss this any further if you like 💪

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the explanation! Understand the reasoning better now and agree it makes sense to proceed like this 👍 Tbh the main downside I saw of this structure was about readability and separation of responsibilities, not even much related to performance. Happy to discuss more on other use cases and even pair together at some point 🙌

Comment on lines +64 to +68
let nudgeCardStyle = NudgeCardStyle(backgroundColor: Color(theme.colors.ecosia.barBackground),
textPrimaryColor: Color(theme.colors.ecosia.textPrimary),
textSecondaryColor: Color(theme.colors.ecosia.textSecondary),
closeButtonTextColor: Color(theme.colors.ecosia.iconDecorative),
actionButtonTextColor: Color(theme.colors.ecosia.buttonBackgroundPrimary))
Copy link
Collaborator

Choose a reason for hiding this comment

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

In addition to the comment above about SwitUI theming, I like this intermediate approach better too, where the view owns their own colors. But I guess this is more related to it being ThemeApplicable instead of Themeable, meaning it's parent will call apply theme and handle responding to theme changes.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree with you on that 💪 . We can elaborate even further if the comment above needs more clarification 🙏

@d4r1091 d4r1091 force-pushed the dc-mob-3199-improve-default-browser-with-settings-card branch from 7c1bf48 to f0898d4 Compare April 14, 2025 13:17
@d4r1091 d4r1091 requested a review from lucaschifino April 14, 2025 13:17
lucaschifino
lucaschifino previously approved these changes Apr 14, 2025
Copy link
Collaborator

@lucaschifino lucaschifino left a comment

Choose a reason for hiding this comment

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

Thanks for the clarifications! All good ✅

Comment on lines 73 to +75
func applyTheme(theme: Theme) {
mainContainerStackView.backgroundColor = theme.colors.ecosia.backgroundSecondary
closeButton.tintColor = theme.colors.ecosia.iconDecorative
titleLabel.textColor = theme.colors.ecosia.textPrimary
descriptionLabel.textColor = theme.colors.ecosia.textSecondary
actionButton.setTitleColor(theme.colors.ecosia.buttonBackgroundPrimary, for: .normal)
guard let viewModel else { return }
configure(with: viewModel, theme: theme)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the explanation! Understand the reasoning better now and agree it makes sense to proceed like this 👍 Tbh the main downside I saw of this structure was about readability and separation of responsibilities, not even much related to performance. Happy to discuss more on other use cases and even pair together at some point 🙌


public static func makeDefaultCoordinatorAndShowDetailViewFrom(_ navigationController: UINavigationController?,
analyticsLabel: Analytics.Label.DefaultBrowser,
topViewContentBackground: Color,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks! Agreed, we can keep it.

This coordinator also lies under the Ecosia framework and cannot access the EcosiaColor constants.

This one issue I think we'll need to fix in the future, but no need now since there are other reasons behind it 🙏

d4r1091 added 22 commits April 15, 2025 09:44
Implemented by moving the NTPConfigurableNudgeCard into SwiftUI and leverage its details.
- Modify the `AppSettingsTableViewController` so that implements the changes needed to display the custom card first and the classic one later
- Replace the `Experiment`naming for a more convenient `Detail` one
- Implemented Analytics to track all the click and dismissal event
- Added animation and configured to play correctly
Reset it's settings back to standard Firefox to reduce conflicts
@d4r1091 d4r1091 force-pushed the dc-mob-3199-improve-default-browser-with-settings-card branch from 06c5a28 to 56e52aa Compare April 15, 2025 13:04
@d4r1091 d4r1091 requested a review from lucaschifino April 15, 2025 13:04
@d4r1091
Copy link
Member Author

d4r1091 commented Apr 15, 2025

@lucaschifino updated and tested the work with the EcosiaFloatDesignSystemFoundations additions 🙌

Copy link
Collaborator

@lucaschifino lucaschifino left a comment

Choose a reason for hiding this comment

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

🎨 ✅ ✨

@d4r1091 d4r1091 merged commit 27bca4e into main Apr 15, 2025
2 of 3 checks passed
@d4r1091 d4r1091 deleted the dc-mob-3199-improve-default-browser-with-settings-card branch April 15, 2025 13:48
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.

2 participants