mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 13:02:14 -06:00
PM-10280 - Implement Autofill Setup Completion Screen (#981)
This commit is contained in:
parent
1b45969eeb
commit
c35520f9db
@ -10,6 +10,9 @@ enum ExternalLinksConstants {
|
||||
/// A link to Bitwarden's organizations information webpage.
|
||||
static let aboutOrganizations = URL(string: "https://bitwarden.com/help/about-organizations")!
|
||||
|
||||
/// A deep link to the Bitwarden app.
|
||||
static let appDeepLink = URL(string: "bitwarden://")!
|
||||
|
||||
/// A link to the app review page within the app store.
|
||||
static let appReview = URL(string: "https://itunes.apple.com/us/app/id1137397744?action=write-review")
|
||||
|
||||
|
||||
@ -9,6 +9,11 @@ extension String {
|
||||
return range(of: regex, options: .regularExpression) != nil
|
||||
}
|
||||
|
||||
/// A Boolean value indicating whether the string represents the "otpauth" scheme.
|
||||
var isOtpAuthScheme: Bool {
|
||||
self == "otpauth"
|
||||
}
|
||||
|
||||
/// `true` if prefixed with `steam://` and followed by a base 32 string.
|
||||
var isSteamUri: Bool {
|
||||
guard let keyIndexOffset = steamURIKeyIndexOffset else {
|
||||
|
||||
@ -64,7 +64,7 @@ public struct OTPAuthModel: Equatable, Hashable, Sendable {
|
||||
///
|
||||
init?(otpAuthKey: String) {
|
||||
guard let urlComponents = URLComponents(string: otpAuthKey),
|
||||
urlComponents.scheme == "otpauth",
|
||||
urlComponents.scheme?.isOtpAuthScheme == true,
|
||||
let queryItems = urlComponents.queryItems,
|
||||
let secret = queryItems.first(where: { $0.name == "secret" })?.value else {
|
||||
return nil
|
||||
|
||||
@ -64,6 +64,18 @@ class AppModuleTests: BitwardenTestCase {
|
||||
XCTAssertTrue(navigationController.viewControllers[0] is UIHostingController<DebugMenuView>)
|
||||
}
|
||||
|
||||
/// `makeExtensionSetupCoordinator` builds the extensions setup coordinator.
|
||||
@MainActor
|
||||
func test_makeExtensionSetupCoordinator() {
|
||||
let navigationController = UINavigationController()
|
||||
let coordinator = subject.makeExtensionSetupCoordinator(
|
||||
stackNavigator: navigationController
|
||||
)
|
||||
coordinator.navigate(to: .extensionActivation(type: .autofillExtension))
|
||||
XCTAssertEqual(navigationController.viewControllers.count, 1)
|
||||
XCTAssertTrue(navigationController.viewControllers[0] is UIHostingController<ExtensionActivationView>)
|
||||
}
|
||||
|
||||
/// `makeSendCoordinator()` builds the send coordinator.
|
||||
@MainActor
|
||||
func test_makeSendCoordinator() {
|
||||
|
||||
@ -102,6 +102,8 @@ public class AppProcessor {
|
||||
/// - Parameter url: The deep link URL to handle.
|
||||
///
|
||||
public func openUrl(_ url: URL) async {
|
||||
guard url.scheme?.isOtpAuthScheme == true else { return }
|
||||
|
||||
guard let otpAuthModel = OTPAuthModel(otpAuthKey: url.absoluteString) else {
|
||||
coordinator?.showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))
|
||||
return
|
||||
|
||||
@ -402,10 +402,20 @@ class AppProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_body
|
||||
XCTAssertEqual(coordinator.events, [.setAuthCompletionRoute(.tab(.vault(.vaultItemSelection(model))))])
|
||||
}
|
||||
|
||||
/// `openUrl(_:)` handles receiving an non OTP deep link and silently returns with a no-op.
|
||||
@MainActor
|
||||
func test_openUrl_nonOtpKey_failSilently() async throws {
|
||||
try await subject.openUrl(XCTUnwrap(URL(string: "bitwarden://")))
|
||||
|
||||
XCTAssertEqual(coordinator.alertShown, [])
|
||||
XCTAssertEqual(coordinator.routes, [])
|
||||
}
|
||||
|
||||
/// `openUrl(_:)` handles receiving an OTP deep link if the URL isn't an OTP key.
|
||||
@MainActor
|
||||
func test_openUrl_otpKey_invalid() async throws {
|
||||
try await subject.openUrl(XCTUnwrap(URL(string: "https://google.com")))
|
||||
let otpKey: String = .otpAuthUriKeyNoSecret
|
||||
try await subject.openUrl(XCTUnwrap(URL(string: otpKey)))
|
||||
|
||||
XCTAssertEqual(coordinator.alertShown, [.defaultAlert(title: Localizations.anErrorHasOccurred)])
|
||||
XCTAssertEqual(coordinator.routes, [])
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "autofill-illustration.pdf",
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "autofill-illustration-dark.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@ -101,7 +101,7 @@
|
||||
"EditItem" = "Edit item";
|
||||
"EnableAutomaticSyncing" = "Allow automatic syncing";
|
||||
"EnterEmailForHint" = "Enter your account email address to receive your master password hint.";
|
||||
"ExntesionReenable" = "Reactivate app extension";
|
||||
"ReactivateAppExtension" = "Reactivate app extension";
|
||||
"ExtensionAlmostDone" = "Almost done!";
|
||||
"ExtensionEnable" = "Activate app extension";
|
||||
"ExtensionInSafari" = "In Safari, find Bitwarden using the share icon (hint: scroll to the right on the bottom row of the menu).";
|
||||
@ -984,5 +984,9 @@
|
||||
"Confirm" = "Confirm";
|
||||
"ErrorConnectingWithTheDuoServiceUseADifferentTwoStepLoginMethodOrContactDuoForAssistance" = "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance.";
|
||||
"ThePreAuthUrlsCouldNotBeLoadedToStartTheAccountCreation" = "The Pre Auth Urls could not be loaded to start the account creation.";
|
||||
"ContinueToBitwarden" = "Continue to Bitwarden";
|
||||
"BackToSettings" = "Back to settings";
|
||||
"YoureAllSet" = "You're all set!";
|
||||
"AutoFillActivatedDescriptionLong" = "You can now use autofill to log into apps and websites using your saved passwords. Now, you can explore everything else Bitwarden has to offer.";
|
||||
"GetStarted" = "Get started";
|
||||
"Dismiss" = "Dismiss";
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
// MARK: - ExtensionActivationEffect
|
||||
|
||||
/// The enumeration of possible effects performed by the `ExtensionActivationProcessor`.
|
||||
///
|
||||
enum ExtensionActivationEffect: Equatable {
|
||||
/// The extension activation view appeared.
|
||||
case appeared
|
||||
}
|
||||
@ -5,35 +5,63 @@
|
||||
class ExtensionActivationProcessor: StateProcessor<
|
||||
ExtensionActivationState,
|
||||
ExtensionActivationAction,
|
||||
Void
|
||||
ExtensionActivationEffect
|
||||
> {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasConfigService
|
||||
|
||||
// MARK: Private Properties
|
||||
|
||||
/// A delegate used to communicate with the app extension.
|
||||
private weak var appExtensionDelegate: AppExtensionDelegate?
|
||||
|
||||
/// The services used by the processor.
|
||||
private let services: Services
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `ExtensionActivationProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appExtensionDelegate: A delegate used to communicate with the app extension.
|
||||
/// - services: The services used by the processor.
|
||||
/// - state: The initial state of the processor.
|
||||
///
|
||||
init(
|
||||
appExtensionDelegate: AppExtensionDelegate?,
|
||||
services: Services,
|
||||
state: ExtensionActivationState
|
||||
) {
|
||||
self.appExtensionDelegate = appExtensionDelegate
|
||||
self.services = services
|
||||
super.init(state: state)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
override func perform(_ effect: ExtensionActivationEffect) async {
|
||||
switch effect {
|
||||
case .appeared:
|
||||
await loadFeatureFlag()
|
||||
}
|
||||
}
|
||||
|
||||
override func receive(_ action: ExtensionActivationAction) {
|
||||
switch action {
|
||||
case .cancelTapped:
|
||||
appExtensionDelegate?.didCancel()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
/// Sets the feature flag value to be used.
|
||||
///
|
||||
private func loadFeatureFlag() async {
|
||||
state.isNativeCreateAccountFeatureFlagEnabled = await services.configService.getFeatureFlag(
|
||||
.nativeCreateAccountFlow,
|
||||
isPreAuth: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,10 +2,13 @@ import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
// MARK: - ExtensionActivationProcessorTests
|
||||
|
||||
class ExtensionActivationProcessorTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var appExtensionDelegate: MockAppExtensionDelegate!
|
||||
var configService: MockConfigService!
|
||||
var subject: ExtensionActivationProcessor!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
@ -14,9 +17,10 @@ class ExtensionActivationProcessorTests: BitwardenTestCase {
|
||||
super.setUp()
|
||||
|
||||
appExtensionDelegate = MockAppExtensionDelegate()
|
||||
|
||||
configService = MockConfigService()
|
||||
subject = ExtensionActivationProcessor(
|
||||
appExtensionDelegate: appExtensionDelegate,
|
||||
services: ServiceContainer.withMocks(configService: configService),
|
||||
state: ExtensionActivationState(extensionType: .autofillExtension)
|
||||
)
|
||||
}
|
||||
@ -25,6 +29,7 @@ class ExtensionActivationProcessorTests: BitwardenTestCase {
|
||||
super.tearDown()
|
||||
|
||||
appExtensionDelegate = nil
|
||||
configService = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
@ -37,4 +42,34 @@ class ExtensionActivationProcessorTests: BitwardenTestCase {
|
||||
|
||||
XCTAssertTrue(appExtensionDelegate.didCancelCalled)
|
||||
}
|
||||
|
||||
/// `perform(.appeared)` with feature flag for .nativeCreateAccountFlow set to true
|
||||
@MainActor
|
||||
func test_perform_appeared_loadFeatureFlag_true() async {
|
||||
configService.featureFlagsBool[.nativeCreateAccountFlow] = true
|
||||
subject.state.isNativeCreateAccountFeatureFlagEnabled = false
|
||||
|
||||
await subject.perform(.appeared)
|
||||
XCTAssertTrue(subject.state.isNativeCreateAccountFeatureFlagEnabled)
|
||||
}
|
||||
|
||||
/// `perform(.appeared)` with feature flag for .nativeCreateAccountFlow set to false
|
||||
@MainActor
|
||||
func test_perform_appeared_loadsFeatureFlag_false() async {
|
||||
configService.featureFlagsBool[.nativeCreateAccountFlow] = false
|
||||
subject.state.isNativeCreateAccountFeatureFlagEnabled = true
|
||||
|
||||
await subject.perform(.appeared)
|
||||
XCTAssertFalse(subject.state.isNativeCreateAccountFeatureFlagEnabled)
|
||||
}
|
||||
|
||||
/// `perform(.appeared)` with feature flag defaulting to false
|
||||
@MainActor
|
||||
func test_perform_appeared_loadsFeatureFlag_nil() async {
|
||||
configService.featureFlagsBool[.nativeCreateAccountFlow] = nil
|
||||
subject.state.isNativeCreateAccountFeatureFlagEnabled = true
|
||||
|
||||
await subject.perform(.appeared)
|
||||
XCTAssertFalse(subject.state.isNativeCreateAccountFeatureFlagEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,4 +7,42 @@ struct ExtensionActivationState: Equatable, Sendable {
|
||||
|
||||
/// The type of extension to show the activation view for.
|
||||
var extensionType: ExtensionActivationType
|
||||
|
||||
/// Whether the native create account feature flag is on.
|
||||
var isNativeCreateAccountFeatureFlagEnabled = false
|
||||
|
||||
/// The message text in the view.
|
||||
var message: String {
|
||||
switch extensionType {
|
||||
case .appExtension:
|
||||
Localizations.extensionSetup +
|
||||
.newLine +
|
||||
Localizations.extensionSetup2
|
||||
case .autofillExtension:
|
||||
Localizations.autofillSetup +
|
||||
.newLine +
|
||||
Localizations.autofillSetup2
|
||||
}
|
||||
}
|
||||
|
||||
/// The title for the navigation bar.
|
||||
var navigationBarTitle: String {
|
||||
guard isNativeCreateAccountFeatureFlagEnabled else { return "" }
|
||||
return extensionType == .autofillExtension ? Localizations.accountSetup : ""
|
||||
}
|
||||
|
||||
/// Whether or not to show the new or legacy view.
|
||||
var showLegacyView: Bool {
|
||||
!isNativeCreateAccountFeatureFlagEnabled || extensionType == .appExtension
|
||||
}
|
||||
|
||||
/// The title text in the view.
|
||||
var title: String {
|
||||
switch extensionType {
|
||||
case .appExtension:
|
||||
Localizations.extensionActivated
|
||||
case .autofillExtension:
|
||||
Localizations.autofillActivated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,34 +8,83 @@ struct ExtensionActivationView: View {
|
||||
// MARK: Properties
|
||||
|
||||
/// The `Store` for this view.
|
||||
@ObservedObject var store: Store<ExtensionActivationState, ExtensionActivationAction, Void>
|
||||
@ObservedObject var store: Store<
|
||||
ExtensionActivationState,
|
||||
ExtensionActivationAction,
|
||||
ExtensionActivationEffect
|
||||
>
|
||||
|
||||
/// The title text in the view.
|
||||
var title: String {
|
||||
switch store.state.extensionType {
|
||||
case .appExtension:
|
||||
Localizations.extensionActivated
|
||||
case .autofillExtension:
|
||||
Localizations.autofillActivated
|
||||
/// An action that opens URLs.
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if store.state.showLegacyView {
|
||||
legacyContent
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
.scrollView()
|
||||
.navigationTitle(store.state.navigationBarTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
await store.perform(.appeared)
|
||||
}
|
||||
}
|
||||
|
||||
/// The message text in the view.
|
||||
var message: String {
|
||||
switch store.state.extensionType {
|
||||
case .appExtension:
|
||||
Localizations.extensionSetup +
|
||||
.newLine +
|
||||
Localizations.extensionSetup2
|
||||
case .autofillExtension:
|
||||
Localizations.autofillSetup +
|
||||
.newLine +
|
||||
Localizations.autofillSetup2
|
||||
// MARK: Private Views
|
||||
|
||||
/// The main content of the view.
|
||||
@ViewBuilder private var content: some View {
|
||||
VStack(spacing: 0) {
|
||||
PageHeaderView(
|
||||
image: Asset.Images.autofillIllustration,
|
||||
title: Localizations.youreAllSet,
|
||||
message: Localizations.autoFillActivatedDescriptionLong
|
||||
)
|
||||
|
||||
Button(Localizations.continueToBitwarden) {
|
||||
openURL(ExternalLinksConstants.appDeepLink)
|
||||
}
|
||||
.buttonStyle(.primary())
|
||||
.padding(.top, 40)
|
||||
|
||||
Button(Localizations.backToSettings) {
|
||||
store.send(.cancelTapped)
|
||||
}
|
||||
.buttonStyle(.transparent)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
}
|
||||
|
||||
/// The legacy view for this screen kept intact to support both versions.
|
||||
@ViewBuilder private var legacyContent: some View {
|
||||
VStack(spacing: 64) {
|
||||
VStack(spacing: 20) {
|
||||
Text(store.state.title)
|
||||
.foregroundStyle(Asset.Colors.textPrimary.swiftUIColor)
|
||||
.styleGuide(.title3)
|
||||
|
||||
Text(store.state.message)
|
||||
.foregroundStyle(Asset.Colors.textSecondary.swiftUIColor)
|
||||
.styleGuide(.body)
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
image
|
||||
}
|
||||
.toolbar {
|
||||
cancelToolbarItem {
|
||||
store.send(.cancelTapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The image to display in the view.
|
||||
@ViewBuilder var image: some View {
|
||||
@ViewBuilder private var image: some View {
|
||||
switch store.state.extensionType {
|
||||
case .appExtension:
|
||||
Image(decorative: Asset.Images.bwLogo)
|
||||
@ -53,42 +102,19 @@ struct ExtensionActivationView: View {
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 64) {
|
||||
VStack(spacing: 20) {
|
||||
Text(title)
|
||||
.foregroundStyle(Asset.Colors.textPrimary.swiftUIColor)
|
||||
.styleGuide(.title3)
|
||||
|
||||
Text(message)
|
||||
.foregroundStyle(Asset.Colors.textSecondary.swiftUIColor)
|
||||
.styleGuide(.body)
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
image
|
||||
}
|
||||
.scrollView()
|
||||
.toolbar {
|
||||
cancelToolbarItem {
|
||||
store.send(.cancelTapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#if DEBUG
|
||||
#Preview("Autofill Extension") {
|
||||
NavigationView {
|
||||
ExtensionActivationView(
|
||||
store: Store(
|
||||
processor: StateProcessor(
|
||||
state: ExtensionActivationState(
|
||||
extensionType: .autofillExtension
|
||||
extensionType: .autofillExtension,
|
||||
isNativeCreateAccountFeatureFlagEnabled: true
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -109,3 +135,4 @@ struct ExtensionActivationView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -3,10 +3,16 @@ import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
// MARK: - ExtensionActivationViewTests
|
||||
|
||||
class ExtensionActivationViewTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var processor: MockProcessor<ExtensionActivationState, ExtensionActivationAction, Void>!
|
||||
var processor: MockProcessor<
|
||||
ExtensionActivationState,
|
||||
ExtensionActivationAction,
|
||||
ExtensionActivationEffect
|
||||
>!
|
||||
var subject: ExtensionActivationView!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
@ -37,6 +43,15 @@ class ExtensionActivationViewTests: BitwardenTestCase {
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .cancelTapped)
|
||||
}
|
||||
|
||||
/// Tapping the back to settings dispatches the `.cancelTapped` action.
|
||||
@MainActor
|
||||
func test_backToSettingsButton_tap() throws {
|
||||
processor.state.isNativeCreateAccountFeatureFlagEnabled = true
|
||||
let button = try subject.inspect().find(button: Localizations.backToSettings)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .cancelTapped)
|
||||
}
|
||||
|
||||
// MARK: Snapshots
|
||||
|
||||
/// The autofill extension activation view renders correctly.
|
||||
@ -56,4 +71,14 @@ class ExtensionActivationViewTests: BitwardenTestCase {
|
||||
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5]
|
||||
)
|
||||
}
|
||||
|
||||
/// The app extension activation view renders correctly when the `native-create-account-flow` ff is on.
|
||||
@MainActor
|
||||
func test_snapshot_extensionActivationView_autoFillExtension_featureFlagEnabled() {
|
||||
processor.state.isNativeCreateAccountFeatureFlagEnabled = true
|
||||
assertSnapshots(
|
||||
of: subject.navStackWrapped,
|
||||
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,9 +17,9 @@ class ExtensionSetupCoordinatorTests: BitwardenTestCase {
|
||||
super.setUp()
|
||||
|
||||
stackNavigator = MockStackNavigator()
|
||||
|
||||
subject = ExtensionSetupCoordinator(
|
||||
appExtensionDelegate: MockAppExtensionDelegate(),
|
||||
services: ServiceContainer.withMocks(),
|
||||
stackNavigator: stackNavigator
|
||||
)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
@ -3,6 +3,15 @@
|
||||
/// A coordinator that manages navigation in the vault tab.
|
||||
///
|
||||
final class ExtensionSetupCoordinator: Coordinator, HasStackNavigator {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasConfigService
|
||||
|
||||
// MARK: Private Properties
|
||||
|
||||
/// The services used by this coordinator.
|
||||
private let services: Services
|
||||
|
||||
// MARK: Private Properties
|
||||
|
||||
/// A delegate used to communicate with the app extension.
|
||||
@ -19,13 +28,16 @@ final class ExtensionSetupCoordinator: Coordinator, HasStackNavigator {
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appExtensionDelegate: A delegate used to communicate with the app extension.
|
||||
/// - services: The services used by this coordinator.
|
||||
/// - stackNavigator: The stack navigator that is managed by this coordinator.
|
||||
///
|
||||
init(
|
||||
appExtensionDelegate: AppExtensionDelegate?,
|
||||
services: Services,
|
||||
stackNavigator: StackNavigator
|
||||
) {
|
||||
self.appExtensionDelegate = appExtensionDelegate
|
||||
self.services = services
|
||||
self.stackNavigator = stackNavigator
|
||||
}
|
||||
|
||||
@ -47,6 +59,7 @@ final class ExtensionSetupCoordinator: Coordinator, HasStackNavigator {
|
||||
private func showExtensionActivation(extensionType: ExtensionActivationType) {
|
||||
let processor = ExtensionActivationProcessor(
|
||||
appExtensionDelegate: appExtensionDelegate,
|
||||
services: services,
|
||||
state: ExtensionActivationState(extensionType: extensionType)
|
||||
)
|
||||
let view = ExtensionActivationView(store: Store(processor: processor))
|
||||
|
||||
@ -23,6 +23,7 @@ extension DefaultAppModule: ExtensionSetupModule {
|
||||
) -> AnyCoordinator<ExtensionSetupRoute, Void> {
|
||||
ExtensionSetupCoordinator(
|
||||
appExtensionDelegate: appExtensionDelegate,
|
||||
services: services,
|
||||
stackNavigator: stackNavigator
|
||||
).asAnyCoordinator()
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ struct AppExtensionView: View {
|
||||
private var activateButton: some View {
|
||||
Button(
|
||||
store.state.extensionEnabled ?
|
||||
Localizations.exntesionReenable :
|
||||
Localizations.reactivateAppExtension :
|
||||
Localizations.extensionEnable
|
||||
) {
|
||||
store.send(.activateButtonTapped)
|
||||
|
||||
@ -9,6 +9,7 @@ extension String {
|
||||
static let otpAuthUriKeyPartial = "otpauth://totp/Example:user@bitwarden.com?secret=JBSWY3DPEHPK3PXP"
|
||||
// swiftlint:disable:next line_length
|
||||
static let otpAuthUriKeySHA512 = "otpauth://totp/Example:user@bitwarden.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512"
|
||||
static let otpAuthUriKeyNoSecret = "otpauth://totp/Example:user@bitwarden.com?"
|
||||
static let steamUriKeyIdentifier = "JBSWY3DPEHPK3PXP"
|
||||
static let steamUriKey = "steam://\(steamUriKeyIdentifier)"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user