-
Notifications
You must be signed in to change notification settings - Fork 99
[PM-33849] feat: Add Premium Upgrade view #2523
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
Changes from 22 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
c1b5805
[PM-33849] feat: Add PremiumUpgrade localization strings
andrebispo5 89ba31a
[PM-33849] chore: Add BillingAPIService mock support
andrebispo5 6ad2e9c
[PM-33849] feat: Add PremiumUpgrade view
andrebispo5 a124c37
[PM-33849] test: Add PremiumUpgrade tests
andrebispo5 f323ebf
[PM-33849] feat: Integrate PremiumUpgrade into Vault
andrebispo5 8ce065b
[PM-33849] feat: Add BillingService with checkout session validation
andrebispo5 447f1bd
[PM-33849] feat: Integrate BillingService into ServiceContainer
andrebispo5 e7252dc
[PM-33849] feat: Update PremiumUpgrade to use BillingService
andrebispo5 04a0092
Merge branch 'main' into pm-33849/premium-upgrade-view
andrebispo5 278ba64
Merge branch 'main' into pm-33849/premium-upgrade-view
andrebispo5 9fc9aee
Merge branch 'main' into pm-33849/premium-upgrade-view
andrebispo5 fb89588
[PM-33849] fix: Remove trailing whitespace
andrebispo5 688eaac
[PM-33849] fix: Correct TODO comment format in BillingError
andrebispo5 15a12c4
[PM-33849] refactor: Inline return in ServiceContainer+Mocks
andrebispo5 7fa8ff8
[PM-33849] fix: Remove unused import in PremiumUpgradeCoordinatorTests
andrebispo5 e100ee0
[PM-33849] fix: Move featureRow to Private Methods section in Premiumβ¦
andrebispo5 f5f0a5b
[PM-33849] refactor: Extract handleAppReviewPromptShown to fix functiβ¦
andrebispo5 e52e970
[PM-33849] fix: Update localization strings for premium upgrade
andrebispo5 d913840
Merge branch 'main' into pm-33849/premium-upgrade-view
andrebispo5 7f43801
Merge branch 'pm-33849/premium-upgrade-view' of https://github.com/biβ¦
andrebispo5 a0fcfba
[PM-33849] test: Add ViewInspector tests for PremiumUpgradeView
andrebispo5 408ecd6
[PM-33849] test: Add snapshot tests for PremiumUpgradeView
andrebispo5 aacf169
[PM-33849] Add security comment explaining HTTPS check in BillingService
andrebispo5 79e1d97
[PM-33849] Convert BillingServiceTests to Swift Testing framework
andrebispo5 53b8bb4
[PM-33849] Fix alphabetization in ServiceContainer
andrebispo5 051deb2
[PM-33849] Group upgradeNowTapped tests together in PremiumUpgradeProβ¦
andrebispo5 7700c60
[PM-33849] Add scroll view and use ContentBlock in PremiumUpgradeView
andrebispo5 ceb731e
[PM-33849] Fix alphabetization in VaultListAction and VaultRoute
andrebispo5 843be90
[PM-33849] Fix conflicts
andrebispo5 1655d9d
Merge branch 'main' into pm-33849/premium-upgrade-view
andrebispo5 e77d233
[PM-33849] Fix localization after merge
andrebispo5 f56b88d
[PM-34897] Fix BillingServiceTests build errors
andrebispo5 3fb2d32
[PM-33849] Fix margin bot in main content block
andrebispo5 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
BitwardenShared/Core/Billing/Models/Enum/BillingError.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import Foundation | ||
|
|
||
| // MARK: - BillingError | ||
|
|
||
| /// Errors that can occur during billing operations. | ||
| /// | ||
| enum BillingError: LocalizedError { | ||
| /// The checkout URL is invalid (e.g., not HTTPS). | ||
| case invalidCheckoutUrl | ||
|
|
||
| /// Unable to open the checkout URL in the browser. | ||
| case unableToOpenCheckout | ||
|
|
||
| var errorDescription: String? { | ||
| switch self { | ||
| case .invalidCheckoutUrl, | ||
| .unableToOpenCheckout: | ||
| // TODO: PM-33856 Handle payment errors | ||
| nil | ||
| } | ||
| } | ||
| } |
46 changes: 46 additions & 0 deletions
46
BitwardenShared/Core/Billing/Services/BillingService.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import Foundation | ||
|
|
||
| // MARK: - BillingService | ||
|
|
||
| /// A protocol for a service used to manage billing operations. | ||
| /// | ||
| protocol BillingService: AnyObject { // sourcery: AutoMockable | ||
| /// Creates a checkout session for premium upgrade and returns the checkout URL. | ||
| /// | ||
| /// - Returns: A validated HTTPS URL for the checkout session. | ||
| /// - Throws: `BillingError.invalidCheckoutUrl` if the URL is invalid or not HTTPS. | ||
| /// | ||
| func createCheckoutSession() async throws -> URL | ||
| } | ||
|
|
||
| // MARK: - DefaultBillingService | ||
|
|
||
| /// The default implementation of `BillingService`. | ||
| /// | ||
| class DefaultBillingService: BillingService { | ||
| // MARK: Properties | ||
|
|
||
| /// The API service used for billing requests. | ||
| private let billingAPIService: BillingAPIService | ||
|
|
||
| // MARK: Initialization | ||
|
|
||
| /// Creates a new `DefaultBillingService`. | ||
| /// | ||
| /// - Parameter billingAPIService: The API service used for billing requests. | ||
| /// | ||
| init(billingAPIService: BillingAPIService) { | ||
| self.billingAPIService = billingAPIService | ||
| } | ||
|
|
||
| // MARK: Methods | ||
|
|
||
| func createCheckoutSession() async throws -> URL { | ||
| let response = try await billingAPIService.createCheckoutSession() | ||
| let url = response.checkoutSessionUrl | ||
| guard url.scheme == "https" else { | ||
| throw BillingError.invalidCheckoutUrl | ||
matt-livefront marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| return url | ||
| } | ||
| } | ||
82 changes: 82 additions & 0 deletions
82
BitwardenShared/Core/Billing/Services/BillingServiceTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import TestHelpers | ||
| import XCTest | ||
matt-livefront marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| @testable import BitwardenShared | ||
| @testable import BitwardenSharedMocks | ||
|
|
||
| class BillingServiceTests: BitwardenTestCase { | ||
| // MARK: Properties | ||
|
|
||
| var billingAPIService: MockBillingAPIService! | ||
| var subject: DefaultBillingService! | ||
|
|
||
| // MARK: Setup and Teardown | ||
|
|
||
| override func setUp() { | ||
| super.setUp() | ||
|
|
||
| billingAPIService = MockBillingAPIService() | ||
| subject = DefaultBillingService(billingAPIService: billingAPIService) | ||
| } | ||
|
|
||
| override func tearDown() { | ||
| super.tearDown() | ||
|
|
||
| billingAPIService = nil | ||
| subject = nil | ||
| } | ||
|
|
||
| // MARK: Tests | ||
|
|
||
| /// `createCheckoutSession()` returns the URL when it uses HTTPS scheme. | ||
| func test_createCheckoutSession_success() async throws { | ||
| let expectedURL = URL(string: "https://checkout.stripe.com/session")! | ||
| billingAPIService.createCheckoutSessionReturnValue = CheckoutSessionResponseModel( | ||
| checkoutSessionUrl: expectedURL, | ||
| ) | ||
|
|
||
| let result = try await subject.createCheckoutSession() | ||
|
|
||
| XCTAssertEqual(billingAPIService.createCheckoutSessionCallsCount, 1) | ||
| XCTAssertEqual(result, expectedURL) | ||
| } | ||
|
|
||
| /// `createCheckoutSession()` throws `invalidCheckoutUrl` when the URL uses HTTP scheme. | ||
| func test_createCheckoutSession_invalidUrl_http() async throws { | ||
| let httpURL = URL(string: "http://checkout.stripe.com/session")! | ||
| billingAPIService.createCheckoutSessionReturnValue = CheckoutSessionResponseModel( | ||
| checkoutSessionUrl: httpURL, | ||
| ) | ||
|
|
||
| await assertAsyncThrows(error: BillingError.invalidCheckoutUrl) { | ||
| _ = try await subject.createCheckoutSession() | ||
| } | ||
|
|
||
| XCTAssertEqual(billingAPIService.createCheckoutSessionCallsCount, 1) | ||
| } | ||
|
|
||
| /// `createCheckoutSession()` throws `invalidCheckoutUrl` when the URL has no scheme. | ||
| func test_createCheckoutSession_invalidUrl_noScheme() async throws { | ||
| let noSchemeURL = URL(string: "checkout.stripe.com/session")! | ||
| billingAPIService.createCheckoutSessionReturnValue = CheckoutSessionResponseModel( | ||
| checkoutSessionUrl: noSchemeURL, | ||
| ) | ||
|
|
||
| await assertAsyncThrows(error: BillingError.invalidCheckoutUrl) { | ||
| _ = try await subject.createCheckoutSession() | ||
| } | ||
|
|
||
| XCTAssertEqual(billingAPIService.createCheckoutSessionCallsCount, 1) | ||
| } | ||
|
|
||
| /// `createCheckoutSession()` propagates errors from the API service. | ||
| func test_createCheckoutSession_apiError() async throws { | ||
| billingAPIService.createCheckoutSessionThrowableError = URLError(.notConnectedToInternet) | ||
|
|
||
| await assertAsyncThrows { | ||
| _ = try await subject.createCheckoutSession() | ||
| } | ||
|
|
||
| XCTAssertEqual(billingAPIService.createCheckoutSessionCallsCount, 1) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeAction.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // MARK: - PremiumUpgradeAction | ||
|
|
||
| /// Actions handled by the `PremiumUpgradeProcessor`. | ||
| /// | ||
| enum PremiumUpgradeAction: Equatable { | ||
| /// The cancel button was tapped. | ||
| case cancelTapped | ||
|
|
||
| /// Clear the checkout URL after it has been opened. | ||
| case clearURL | ||
|
|
||
| /// The checkout URL failed to open in the browser. | ||
| case urlOpenFailed | ||
| } |
71 changes: 71 additions & 0 deletions
71
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeCoordinator.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import BitwardenKit | ||
| import SwiftUI | ||
|
|
||
| // MARK: - PremiumUpgradeCoordinator | ||
|
|
||
| /// A coordinator that manages navigation for the premium upgrade view. | ||
| /// | ||
| class PremiumUpgradeCoordinator: Coordinator, HasStackNavigator { | ||
| // MARK: Types | ||
|
|
||
| typealias Services = HasBillingService | ||
| & HasErrorAlertServices.ErrorAlertServices | ||
| & HasErrorReporter | ||
|
|
||
| // MARK: Properties | ||
|
|
||
| /// The services used by this coordinator. | ||
| let services: Services | ||
|
|
||
| /// The stack navigator that is managed by this coordinator. | ||
| private(set) weak var stackNavigator: StackNavigator? | ||
|
|
||
| // MARK: Initialization | ||
|
|
||
| /// Creates a new `PremiumUpgradeCoordinator`. | ||
| /// | ||
| /// - Parameters: | ||
| /// - services: The services used by this coordinator. | ||
| /// - stackNavigator: The stack navigator that is managed by this coordinator. | ||
| /// | ||
| init( | ||
| services: Services, | ||
| stackNavigator: StackNavigator, | ||
| ) { | ||
| self.services = services | ||
| self.stackNavigator = stackNavigator | ||
| } | ||
|
|
||
| // MARK: Methods | ||
|
|
||
| func navigate(to route: PremiumUpgradeRoute, context: AnyObject?) { | ||
| switch route { | ||
| case .dismiss: | ||
| stackNavigator?.dismiss() | ||
| } | ||
| } | ||
|
|
||
| func start() { | ||
| showPremiumUpgrade() | ||
| } | ||
|
|
||
| // MARK: Private Methods | ||
|
|
||
| /// Shows the premium upgrade screen. | ||
| /// | ||
| private func showPremiumUpgrade() { | ||
| let processor = PremiumUpgradeProcessor( | ||
| coordinator: asAnyCoordinator(), | ||
| services: services, | ||
| state: PremiumUpgradeState(), | ||
| ) | ||
| let view = PremiumUpgradeView(store: Store(processor: processor)) | ||
| stackNavigator?.replace(view) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - HasErrorAlertServices | ||
|
|
||
| extension PremiumUpgradeCoordinator: HasErrorAlertServices { | ||
| var errorAlertServices: ErrorAlertServices { services } | ||
| } |
54 changes: 54 additions & 0 deletions
54
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeCoordinatorTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import BitwardenKitMocks | ||
| import SwiftUI | ||
| import XCTest | ||
|
|
||
| @testable import BitwardenShared | ||
|
|
||
| class PremiumUpgradeCoordinatorTests: BitwardenTestCase { | ||
| // MARK: Properties | ||
|
|
||
| var stackNavigator: MockStackNavigator! | ||
| var subject: PremiumUpgradeCoordinator! | ||
|
|
||
| // MARK: Setup & Teardown | ||
|
|
||
| override func setUp() { | ||
| super.setUp() | ||
|
|
||
| stackNavigator = MockStackNavigator() | ||
|
|
||
| subject = PremiumUpgradeCoordinator( | ||
| services: ServiceContainer.withMocks(), | ||
| stackNavigator: stackNavigator, | ||
| ) | ||
| } | ||
|
|
||
| override func tearDown() { | ||
| super.tearDown() | ||
|
|
||
| stackNavigator = nil | ||
| subject = nil | ||
| } | ||
|
|
||
| // MARK: Tests | ||
|
|
||
| /// `start()` replaces the stack navigator's stack with the premium upgrade view. | ||
| @MainActor | ||
| func test_start() throws { | ||
| subject.start() | ||
|
|
||
| XCTAssertEqual(stackNavigator.actions.count, 1) | ||
| let action = try XCTUnwrap(stackNavigator.actions.last) | ||
| XCTAssertEqual(action.type, .replaced) | ||
| XCTAssertTrue(action.view is PremiumUpgradeView) | ||
| } | ||
|
|
||
| /// `navigate(to:)` with `.dismiss` dismisses the view. | ||
| @MainActor | ||
| func test_navigate_dismiss() throws { | ||
| subject.navigate(to: .dismiss) | ||
|
|
||
| let action = try XCTUnwrap(stackNavigator.actions.last) | ||
| XCTAssertEqual(action.type, .dismissed) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.