Skip to content

Commit a702d3a

Browse files
authored
[PM-3503] Add AnonAddy self-hosted server URL support (#1498)
1 parent 940d81e commit a702d3a

File tree

10 files changed

+64
-1
lines changed

10 files changed

+64
-1
lines changed

BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ enum FeatureFlag: String, CaseIterable, Codable {
88
/// A feature flag to enable/disable account deprovisioning.
99
case accountDeprovisioning = "pm-10308-account-deprovisioning"
1010

11+
/// A feature flag to enable/disable the ability to add a custom domain for anonAddy users.
12+
case anonAddySelfHostAlias = "anon-addy-self-host-alias"
13+
1114
/// A feature flag to enable/disable the app review prompt.
1215
case appReviewPrompt = "app-review-prompt"
1316

@@ -133,6 +136,7 @@ enum FeatureFlag: String, CaseIterable, Codable {
133136
.testLocalInitialStringFlag:
134137
false
135138
case .accountDeprovisioning,
139+
.anonAddySelfHostAlias,
136140
.appReviewPrompt,
137141
.cipherKeyEncryption,
138142
.cxpExportMobile,

BitwardenShared/Core/Platform/Models/Enum/FeatureFlagTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ final class FeatureFlagTests: BitwardenTestCase {
1919

2020
/// `getter:isRemotelyConfigured` returns the correct value for each flag.
2121
func test_isRemotelyConfigured() {
22+
XCTAssertTrue(FeatureFlag.anonAddySelfHostAlias.isRemotelyConfigured)
2223
XCTAssertTrue(FeatureFlag.appReviewPrompt.isRemotelyConfigured)
2324
XCTAssertTrue(FeatureFlag.cipherKeyEncryption.isRemotelyConfigured)
2425
XCTAssertTrue(FeatureFlag.cxpExportMobile.isRemotelyConfigured)

BitwardenShared/Core/Tools/Models/Domain/UsernameGenerationOptions.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ struct UsernameGenerationOptions: Codable, Equatable {
1010
/// The domain name used to generate a forwarded email alias with addy.io.
1111
var anonAddyDomainName: String?
1212

13+
/// The base URL for the addy.io api.
14+
var anonAddyBaseUrl: String?
15+
1316
/// Whether to capitalize the random word.
1417
var capitalizeRandomWordUsername: Bool?
1518

BitwardenShared/Core/Tools/Models/Domain/UsernameGenerationOptionsTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class UsernameGenerationOptionsTests: BitwardenTestCase {
1111
{
1212
"anonAddyApiAccessToken": "ADDYIO_API_TOKEN",
1313
"anonAddyDomainName": "bitwarden.com",
14+
"anonAddyBaseUrl": "bitwarden.com",
1415
"capitalizeRandomWordUsername": true,
1516
"catchAllEmailDomain": "bitwarden.com",
1617
"catchAllEmailType": 0,
@@ -34,6 +35,7 @@ class UsernameGenerationOptionsTests: BitwardenTestCase {
3435
UsernameGenerationOptions(
3536
anonAddyApiAccessToken: "ADDYIO_API_TOKEN",
3637
anonAddyDomainName: "bitwarden.com",
38+
anonAddyBaseUrl: "bitwarden.com",
3739
capitalizeRandomWordUsername: true,
3840
catchAllEmailDomain: "bitwarden.com",
3941
catchAllEmailType: .random,

BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,3 +1167,4 @@
11671167
"ExpiresToday" = "Expires today";
11681168
"ExpiresInXDays" = "Expires in %1$@ days";
11691169
"DateRangeXToY" = "%1$@ to %2$@";
1170+
"SelfHostServerURL" = "Self-host server URL";

BitwardenShared/UI/Tools/Generator/Generator/GeneratorProcessor.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ final class GeneratorProcessor: StateProcessor<GeneratorState, GeneratorAction,
8282
override func perform(_ effect: GeneratorEffect) async {
8383
switch effect {
8484
case .appeared:
85+
await loadAddyIOFeatureFlag()
8586
await reloadGeneratorOptions()
8687
await generateValue(shouldSavePassword: true)
8788
await checkLearnGeneratorActionCardEligibility()
@@ -192,6 +193,13 @@ final class GeneratorProcessor: StateProcessor<GeneratorState, GeneratorAction,
192193

193194
// MARK: Private
194195

196+
/// Checks if the Addy.io feature flag is enabled and updates the state accordingly.
197+
///
198+
private func loadAddyIOFeatureFlag() async {
199+
state.usernameState.addyIOSelfHostServerUrlEnabled = await services.configService
200+
.getFeatureFlag(.anonAddySelfHostAlias)
201+
}
202+
195203
/// Checks the eligibility of the generator Login action card.
196204
///
197205
private func checkLearnGeneratorActionCardEligibility() async {

BitwardenShared/UI/Tools/Generator/Generator/GeneratorProcessorTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,15 @@ class GeneratorProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ
485485
XCTAssertFalse(subject.state.isLearnGeneratorActionCardEligible)
486486
}
487487

488+
/// `perform(:)` with `.appeared` should set the `addyIOSelfHostServerUrlEnabled` to
489+
/// feature flag `anonAddySelfHostAlias` value.
490+
@MainActor
491+
func test_perform_checkAddyIOFeatureFlag_true() async {
492+
configService.featureFlagsBool[.anonAddySelfHostAlias] = true
493+
await subject.perform(.appeared)
494+
XCTAssertTrue(subject.state.usernameState.addyIOSelfHostServerUrlEnabled)
495+
}
496+
488497
/// `receive(_:)` with `.copyGeneratedValØue` copies the generated password to the system
489498
/// pasteboard and shows a toast.
490499
@MainActor

BitwardenShared/UI/Tools/Generator/Generator/GeneratorState+UsernameState.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ extension GeneratorState {
1515

1616
// MARK: Properties
1717

18+
/// A flag indicating if the addy IO selfhost is enabled.
19+
var addyIOSelfHostServerUrlEnabled = false
20+
1821
/// An optional website host used to generate usernames (either plus addressed or catch all).
1922
var emailWebsite: String?
2023

@@ -37,6 +40,9 @@ extension GeneratorState {
3740
/// The domain name used to generate a forwarded email alias with addy.io.
3841
var addyIODomainName: String = ""
3942

43+
/// The base URL for the addy.io api.
44+
var addyIOSelfHostServerUrl: String = ""
45+
4046
/// The DuckDuckGo API key used to generate a forwarded email alias.
4147
var duckDuckGoAPIKey: String = ""
4248

@@ -93,6 +99,7 @@ extension GeneratorState {
9399
// Forwarded Email Properties
94100
addyIOAPIAccessToken = options.anonAddyApiAccessToken ?? addyIOAPIAccessToken
95101
addyIODomainName = options.anonAddyDomainName ?? addyIODomainName
102+
addyIOSelfHostServerUrl = options.anonAddyBaseUrl ?? addyIOSelfHostServerUrl
96103
duckDuckGoAPIKey = options.duckDuckGoApiKey ?? duckDuckGoAPIKey
97104
fastmailAPIKey = options.fastMailApiKey ?? fastmailAPIKey
98105
firefoxRelayAPIAccessToken = options.firefoxRelayApiAccessToken ?? firefoxRelayAPIAccessToken
@@ -162,6 +169,7 @@ extension GeneratorState.UsernameState {
162169
UsernameGenerationOptions(
163170
anonAddyApiAccessToken: addyIOAPIAccessToken.nilIfEmpty,
164171
anonAddyDomainName: addyIODomainName.nilIfEmpty,
172+
anonAddyBaseUrl: addyIOSelfHostServerUrl.nilIfEmpty,
165173
capitalizeRandomWordUsername: capitalize,
166174
catchAllEmailDomain: domain.nilIfEmpty,
167175
catchAllEmailType: catchAllEmailType,
@@ -221,7 +229,7 @@ extension GeneratorState.UsernameState {
221229
ForwarderServiceType.addyIo(
222230
apiToken: addyIOAPIAccessToken,
223231
domain: addyIODomainName,
224-
baseUrl: "https://app.addy.io"
232+
baseUrl: addyIOSelfHostServerUrl.nilIfEmpty ?? "https://app.addy.io"
225233
)
226234
case .duckDuckGo:
227235
ForwarderServiceType.duckDuckGo(token: duckDuckGoAPIKey)

BitwardenShared/UI/Tools/Generator/Generator/GeneratorState.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,15 @@ extension GeneratorState {
413413
title: Localizations.domainNameRequiredParenthesis
414414
),
415415
])
416+
if usernameState.addyIOSelfHostServerUrlEnabled {
417+
fields.append(contentsOf: [
418+
textField(
419+
accessibilityId: "AnonAddySelfHosteUrlEntry",
420+
keyPath: \.usernameState.addyIOSelfHostServerUrl,
421+
title: Localizations.selfHostServerURL
422+
),
423+
])
424+
}
416425
case .duckDuckGo:
417426
fields.append(
418427
textField(

BitwardenShared/UI/Tools/Generator/Generator/GeneratorStateTests.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class GeneratorStateTests: BitwardenTestCase { // swiftlint:disable:this type_bo
100100
subject.generatorType = .username
101101
subject.usernameState.usernameGeneratorType = .forwardedEmail
102102
subject.usernameState.forwardedEmailService = .addyIO
103+
subject.usernameState.addyIOSelfHostServerUrlEnabled = true
103104

104105
assertInlineSnapshot(of: dumpFormSections(subject.formSections), as: .lines) {
105106
"""
@@ -115,6 +116,7 @@ class GeneratorStateTests: BitwardenTestCase { // swiftlint:disable:this type_bo
115116
Options: addy.io, DuckDuckGo, Fastmail, Firefox Relay, ForwardEmail, SimpleLogin
116117
Text: API access token Value: (empty)
117118
Text: Domain name (required) Value: (empty)
119+
Text: Self-host server URL Value: (empty)
118120
"""
119121
}
120122
}
@@ -551,6 +553,9 @@ class GeneratorStateTests: BitwardenTestCase { // swiftlint:disable:this type_bo
551553
subject.addyIOAPIAccessToken = "token"
552554
XCTAssertTrue(subject.canGenerateUsername)
553555

556+
subject.addyIOSelfHostServerUrl = "bitwarden.com"
557+
XCTAssertTrue(subject.canGenerateUsername)
558+
554559
subject.forwardedEmailService = .duckDuckGo
555560
XCTAssertFalse(subject.canGenerateUsername)
556561
subject.duckDuckGoAPIKey = "apiKey"
@@ -587,6 +592,19 @@ class GeneratorStateTests: BitwardenTestCase { // swiftlint:disable:this type_bo
587592
XCTAssertEqual(subject.plusAddressedEmailType, .random)
588593
}
589594

595+
/// `usernameState.update(with:)` sets addy io base url if exists.
596+
func test_usernameState_updateWithOptions_addyIOBaseUrl() {
597+
var subject = GeneratorState().usernameState
598+
subject.update(with: UsernameGenerationOptions(anonAddyBaseUrl: "bitwarden.com"))
599+
600+
XCTAssertEqual(subject.usernameGenerationOptions.anonAddyBaseUrl, "bitwarden.com")
601+
602+
subject.addyIOSelfHostServerUrl = "bitwarden2.com"
603+
subject.update(with: UsernameGenerationOptions())
604+
605+
XCTAssertEqual(subject.usernameGenerationOptions.anonAddyBaseUrl, "bitwarden2.com")
606+
}
607+
590608
/// `usernameState.update(with:)` sets the email type to website if an email website exists.
591609
func test_usernameState_updateWithOptions_website() {
592610
var subject = GeneratorState().usernameState

0 commit comments

Comments
 (0)