Skip to content

Commit 15815b8

Browse files
authored
Merge pull request #7143 from woocommerce/feat/7068-siwa-close-account
SIWA account removal: update action to close account
2 parents 606477b + 5560f00 commit 15815b8

File tree

13 files changed

+160
-74
lines changed

13 files changed

+160
-74
lines changed

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
4242
case .backgroundProductImageUpload:
4343
return buildConfig == .localDeveloper || buildConfig == .alpha
4444
case .appleIDAccountDeletion:
45-
return buildConfig == .localDeveloper || buildConfig == .alpha
45+
return true
4646
default:
4747
return true
4848
}

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
-----
55
- [*] Coupons: Fixed issue saving "Individual Use" and "Exclude Sale Items" fields. [https://github.com/woocommerce/woocommerce-ios/pull/7117]
66
- [*] Orders: The customer shipping/billing address form now navigates back automatically after selecting a country or state. [https://github.com/woocommerce/woocommerce-ios/pull/7119]
7+
- [internal] In settings and empty stores screen, the "Close Account" link is shown for users who signed in with Apple (the only way to create an account) to close their WordPress.com account. [https://github.com/woocommerce/woocommerce-ios/pull/7143]
78

89
9.4
910
-----

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,31 @@ extension WooAnalyticsEvent {
11251125
}
11261126
}
11271127

1128+
// MARK: - Close Account
1129+
//
1130+
extension WooAnalyticsEvent {
1131+
/// The source that presents the Jetpack install screen.
1132+
enum CloseAccountSource: String {
1133+
case settings
1134+
case emptyStores = "empty_stores"
1135+
}
1136+
1137+
/// Tracked when the user taps to close their WordPress.com account.
1138+
static func closeAccountTapped(source: CloseAccountSource) -> WooAnalyticsEvent {
1139+
WooAnalyticsEvent(statName: .closeAccountTapped, properties: ["source": source.rawValue])
1140+
}
1141+
1142+
/// Tracked when the WordPress.com account closure succeeds.
1143+
static func closeAccountSuccess() -> WooAnalyticsEvent {
1144+
WooAnalyticsEvent(statName: .closeAccountSuccess, properties: [:])
1145+
}
1146+
1147+
/// Tracked when the WordPress.com account closure fails.
1148+
static func closeAccountFailed(error: Error) -> WooAnalyticsEvent {
1149+
WooAnalyticsEvent(statName: .closeAccountFailed, properties: [:], error: error)
1150+
}
1151+
}
1152+
11281153
private extension PaymentMethod {
11291154
var analyticsValue: String {
11301155
switch self {

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,11 @@ public enum WooAnalyticsStat: String {
596596
case inboxNotesLoaded = "inbox_notes_loaded"
597597
case inboxNotesLoadedFailed = "inbox_notes_load_failed"
598598
case inboxNoteAction = "inbox_note_action"
599+
600+
// MARK: Close Account
601+
case closeAccountTapped = "close_account_tapped"
602+
case closeAccountSuccess = "close_account_success"
603+
case closeAccountFailed = "close_account_failed"
599604
}
600605

601606
public extension WooAnalyticsStat {

WooCommerce/Classes/Authentication/Epilogue/EmptyStoresTableViewCell.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ final class EmptyStoresTableViewCell: UITableViewCell {
88

99
var onJetpackSetupButtonTapped: (() -> Void)?
1010

11-
var onRemoveAppleIDAccessButtonTapped: (() -> Void)?
11+
var onCloseAccountButtonTapped: (() -> Void)?
1212

1313
/// LegendLabel: To be displayed below the ImageView.
1414
///
@@ -66,9 +66,9 @@ private extension EmptyStoresTableViewCell {
6666

6767
func configureRemoveAppleIDAccessButton() {
6868
removeAppleIDAccessButton.applyLinkButtonStyle()
69-
removeAppleIDAccessButton.setTitle(Localization.removeAppleIDAccessTitle, for: .normal)
69+
removeAppleIDAccessButton.setTitle(Localization.closeAccountTitle, for: .normal)
7070
removeAppleIDAccessButton.on(.touchUpInside) { [weak self] _ in
71-
self?.onRemoveAppleIDAccessButtonTapped?()
71+
self?.onCloseAccountButtonTapped?()
7272
}
7373
}
7474
}
@@ -77,9 +77,9 @@ private extension EmptyStoresTableViewCell {
7777
enum Localization {
7878
static let actionTitle = NSLocalizedString("Connect your store with Jetpack",
7979
comment: "Link on the store picker when there are no stores available. Opens a webview about Jetpack setup.")
80-
static let removeAppleIDAccessTitle = NSLocalizedString(
81-
"Remove Apple ID access",
82-
comment: "Link on the store picker for users who signed in with Apple to disconnect their Apple ID from the app."
80+
static let closeAccountTitle = NSLocalizedString(
81+
"Close Account",
82+
comment: "Link on the store picker for users who signed in with Apple to close their WordPress.com account."
8383
)
8484
static let legend =
8585
NSLocalizedString("If you already have a store, you’ll need to install the free Jetpack plugin and connect it to your WordPress.com account.",

WooCommerce/Classes/Authentication/Epilogue/StorePickerViewController.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,8 +648,9 @@ extension StorePickerViewController: UITableViewDataSource {
648648
&& featureFlagService.isFeatureFlagEnabled(.appleIDAccountDeletion)
649649
cell.updateRemoveAppleIDAccessButtonVisibility(isVisible: isRemoveAppleIDAccessButtonVisible)
650650
if isRemoveAppleIDAccessButtonVisible {
651-
cell.onRemoveAppleIDAccessButtonTapped = { [weak self] in
651+
cell.onCloseAccountButtonTapped = { [weak self] in
652652
guard let self = self else { return }
653+
ServiceLocator.analytics.track(event: .closeAccountTapped(source: .emptyStores))
653654
self.removeAppleIDAccessCoordinator.start()
654655
}
655656
}
@@ -716,8 +717,7 @@ private extension StorePickerViewController {
716717
func removeAppleIDAccess() async -> Result<Void, Error> {
717718
await withCheckedContinuation { [weak self] continuation in
718719
guard let self = self else { return }
719-
let action = AccountAction.removeAppleIDAccess(dotcomAppID: ApiCredentials.dotcomAppId,
720-
dotcomSecret: ApiCredentials.dotcomSecret) { result in
720+
let action = AccountAction.closeAccount { result in
721721
continuation.resume(returning: result)
722722
}
723723
self.stores.dispatch(action)
Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
import UIKit
2+
import protocol Yosemite.StoresManager
23

34
/// Coordinates navigation for removing Apple ID access from various entry points (settings and empty stores screens).
4-
final class RemoveAppleIDAccessCoordinator {
5+
@MainActor final class RemoveAppleIDAccessCoordinator {
56
private let sourceViewController: UIViewController
67
private let removeAction: () async -> Result<Void, Error>
78
private let onRemoveSuccess: () -> Void
9+
private let stores: StoresManager
10+
private let analytics: Analytics
811

912
/// - Parameters:
1013
/// - sourceViewController: the view controller that presents the in-progress UI and alerts.
1114
/// - removeAction: called when the remove action is confirmed.
1215
/// - onRemoveSuccess: called when the removal is successful.
1316
init(sourceViewController: UIViewController,
1417
removeAction: @escaping () async -> Result<Void, Error>,
15-
onRemoveSuccess: @escaping () -> Void) {
18+
onRemoveSuccess: @escaping () -> Void,
19+
stores: StoresManager = ServiceLocator.stores,
20+
analytics: Analytics = ServiceLocator.analytics) {
1621
self.sourceViewController = sourceViewController
1722
self.removeAction = removeAction
1823
self.onRemoveSuccess = onRemoveSuccess
24+
self.stores = stores
25+
self.analytics = analytics
1926
}
2027

2128
func start() {
@@ -26,11 +33,15 @@ final class RemoveAppleIDAccessCoordinator {
2633
private extension RemoveAppleIDAccessCoordinator {
2734
func presentConfirmationAlert() {
2835
// TODO: 7068 - analytics
36+
37+
let username = stores.sessionManager.defaultAccount?.username ?? ""
38+
let message = String.localizedStringWithFormat(Localization.ConfirmationAlert.alertMessageFormat, username)
2939
let alertController = UIAlertController(title: Localization.ConfirmationAlert.alertTitle,
30-
message: Localization.ConfirmationAlert.alertMessage,
40+
message: message,
3141
preferredStyle: .alert)
3242
alertController.addActionWithTitle(Localization.ConfirmationAlert.cancelButtonTitle, style: .cancel) { _ in }
33-
alertController.addActionWithTitle(Localization.ConfirmationAlert.removeButtonTitle, style: .destructive) { [weak self] _ in
43+
let closeAccountAction = alertController.addActionWithTitle(Localization.ConfirmationAlert.removeButtonTitle,
44+
style: .destructive) { [weak self] _ in
3445
guard let self = self else { return }
3546

3647
// TODO: 7068 - analytics
@@ -44,14 +55,28 @@ private extension RemoveAppleIDAccessCoordinator {
4455
guard let self = self else { return }
4556
switch result {
4657
case .success:
58+
self.analytics.track(event: .closeAccountSuccess())
4759
self.onRemoveSuccess()
4860
case .failure(let error):
49-
DDLogError("⛔️ Cannot remove Apple ID access: \(error)")
50-
self.presentErrorAlert()
61+
self.analytics.track(event: .closeAccountFailed(error: error))
62+
DDLogError("⛔️ Cannot close account: \(error)")
63+
self.presentErrorAlert(error: error)
5164
}
5265
}
5366
}
5467
}
68+
69+
// Enables close account button based on whether the username matches the user input.
70+
closeAccountAction.isEnabled = false
71+
72+
// Username text field.
73+
alertController.addTextField { textField in
74+
textField.clearButtonMode = .always
75+
textField.on(.editingChanged) { textField in
76+
closeAccountAction.isEnabled = textField.text == username
77+
}
78+
}
79+
5580
sourceViewController.present(alertController, animated: true)
5681
}
5782

@@ -66,62 +91,102 @@ private extension RemoveAppleIDAccessCoordinator {
6691
sourceViewController.dismiss(animated: true, completion: completion)
6792
}
6893

69-
func presentErrorAlert() {
94+
func presentErrorAlert(error: Error) {
7095
let alertController = UIAlertController(title: Localization.ErrorAlert.alertTitle,
71-
message: Localization.ErrorAlert.alertMessage,
96+
message: generateLocalizedMessage(error: error),
7297
preferredStyle: .alert)
98+
alertController.addActionWithTitle(Localization.ErrorAlert.contactSupportButtonTitle, style: .default) { [weak self] _ in
99+
guard let self = self else { return }
100+
ZendeskProvider.shared.showNewRequestIfPossible(from: self.sourceViewController, with: nil)
101+
}
73102
alertController.addActionWithTitle(Localization.ErrorAlert.dismissButtonTitle, style: .cancel) { _ in }
74103
sourceViewController.present(alertController, animated: true)
75104
}
105+
106+
func generateLocalizedMessage(error: Error) -> String {
107+
let errorCode = errorCode(error: error)
108+
109+
switch errorCode {
110+
case "unauthorized":
111+
return NSLocalizedString("You're not authorized to close the account.",
112+
comment: "Error message displayed when unable to close user account due to being unauthorized.")
113+
case "atomic-site":
114+
return NSLocalizedString("To close this account now, contact our support team.",
115+
comment: "Error message displayed when unable to close user account due to having active atomic site.")
116+
case "chargebacked-site":
117+
return NSLocalizedString("This user account cannot be closed if there are unresolved chargebacks.",
118+
comment: "Error message displayed when unable to close user account due to unresolved chargebacks.")
119+
case "active-subscriptions":
120+
return NSLocalizedString("This user account cannot be closed while it has active subscriptions.",
121+
comment: "Error message displayed when unable to close user account due to having active subscriptions.")
122+
case "active-memberships":
123+
return NSLocalizedString("This user account cannot be closed while it has active purchases.",
124+
comment: "Error message displayed when unable to close user account due to having active purchases.")
125+
default:
126+
return NSLocalizedString("An error occured while closing account.",
127+
comment: "Default error message displayed when unable to close user account.")
128+
}
129+
}
130+
131+
func errorCode(error: Error) -> String? {
132+
let userInfo = (error as NSError).userInfo
133+
let errorCode = userInfo[Constants.dotcomAPIErrorCodeKey] as? String
134+
return errorCode
135+
}
76136
}
77137

78138
private extension RemoveAppleIDAccessCoordinator {
79139
enum Localization {
80140
enum RemoveAppleIDAccessInProgressView {
81141
static let title = NSLocalizedString(
82-
"Removing Apple ID Access...",
83-
comment: "Title of the Remove Apple ID Access in-progress view."
142+
"Closing account...",
143+
comment: "Title of the Close Account in-progress view."
84144
)
85145
}
86146

87147
enum ConfirmationAlert {
88148
static let alertTitle = NSLocalizedString(
89-
"Remove Apple ID Access",
90-
comment: "Remove Apple ID Access confirmation alert title - confirms and revokes Apple ID token."
149+
"Confirm Close Account",
150+
comment: "Close Account confirmation alert title."
91151
)
92-
static let alertMessage = NSLocalizedString(
93-
"This will log you out and reset your Sign In With Apple access token. " +
94-
"You will be asked to re-enter your WordPress.com password if you Sign In With Apple again.",
95-
comment: "Alert message to confirm a user meant to remove Apple ID access."
152+
static let alertMessageFormat = NSLocalizedString(
153+
"To confirm, please re-enter your username before closing.\n\n%1$@",
154+
comment: "Close Account confirmation alert message. The %1$@ is the user's WordPress.com username."
96155
)
97156
static let cancelButtonTitle = NSLocalizedString(
98157
"Cancel",
99-
comment: "Alert button title - dismisses alert, which cancels the Remove Apple ID Access attempt."
158+
comment: "Close Account button title - dismisses alert, which cancels the Close Account attempt."
100159
)
101160

102161
static let removeButtonTitle = NSLocalizedString(
103-
"Remove",
104-
comment: "Remove Apple ID Access button title - confirms and revokes Apple ID token."
162+
"Permanently Close Account",
163+
comment: "Close Account button title - confirms and closes user's WordPress.com account."
105164
)
106165
}
107166

108167
enum ErrorAlert {
109168
static let alertTitle = NSLocalizedString(
110-
"Error Removing Apple ID Access",
111-
comment: "Remove Apple ID Access error alert title."
169+
"Couldn’t close account automatically",
170+
comment: "Error title displayed when unable to close user account."
112171
)
113-
static let alertMessage = NSLocalizedString(
114-
"Please try again or contact us for support.",
115-
comment: "Remove Apple ID Access error alert message."
172+
static let contactSupportButtonTitle = NSLocalizedString(
173+
"Contact Support",
174+
comment: "Close Account error alert button title - navigates the user to contact support."
116175
)
117176
static let dismissButtonTitle = NSLocalizedString(
118-
"OK",
119-
comment: "Alert button title - dismisses Remove Apple ID Access error alert."
177+
"Cancel",
178+
comment: "Close Account error alert button title - dismisses error alert."
120179
)
121180
}
122181
}
123182
}
124183

184+
private extension RemoveAppleIDAccessCoordinator {
185+
enum Constants {
186+
static let dotcomAPIErrorCodeKey = "WordPressComRestApiErrorCodeKey"
187+
}
188+
}
189+
125190
enum RemoveAppleIDAccessError: Error {
126191
case presenterDeallocated
127192
}

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ private extension SettingsViewController {
231231
cell.selectionStyle = .default
232232
cell.textLabel?.textAlignment = .center
233233
cell.textLabel?.textColor = .error
234-
cell.textLabel?.text = Localization.removeAppleIDAccess
234+
cell.textLabel?.text = Localization.closeAccount
235235
}
236236

237237
func configureLogout(cell: BasicTableViewCell) {
@@ -258,14 +258,14 @@ private extension SettingsViewController {
258258
//
259259
private extension SettingsViewController {
260260
func removeAppleIDAccessWasPressed() {
261+
ServiceLocator.analytics.track(event: .closeAccountTapped(source: .settings))
261262
removeAppleIDAccessCoordinator.start()
262263
}
263264

264265
func removeAppleIDAccess() async -> Result<Void, Error> {
265266
await withCheckedContinuation { [weak self] continuation in
266267
guard let self = self else { return }
267-
let action = AccountAction.removeAppleIDAccess(dotcomAppID: ApiCredentials.dotcomAppId,
268-
dotcomSecret: ApiCredentials.dotcomSecret) { result in
268+
let action = AccountAction.closeAccount { result in
269269
continuation.resume(returning: result)
270270
}
271271
self.stores.dispatch(action)
@@ -730,9 +730,9 @@ private extension SettingsViewController {
730730
comment: "Navigates to screen containing the latest WooCommerce Features"
731731
)
732732

733-
static let removeAppleIDAccess = NSLocalizedString(
734-
"Remove Apple ID Access",
735-
comment: "Remove Apple ID Access button title to revoke Apple ID token"
733+
static let closeAccount = NSLocalizedString(
734+
"Close Account",
735+
comment: "Close Account button title to close the user's WordPress.com account"
736736
)
737737

738738
static let logout = NSLocalizedString(

WooCommerce/WooCommerceTests/ViewRelated/Settings/RemoveAppleIDAccessCoordinatorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class RemoveAppleIDAccessCoordinatorTests: XCTestCase {
2828
super.tearDown()
2929
}
3030

31-
func test_alert_is_presented_when_starting_coordinator() throws {
31+
@MainActor func test_alert_is_presented_when_starting_coordinator() throws {
3232
// Given
3333
let coordinator = RemoveAppleIDAccessCoordinator(sourceViewController: sourceViewController) {
3434
return .success(())

Yosemite/Yosemite/Actions/AccountAction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ public enum AccountAction: Action {
1414
case synchronizeSites(selectedSiteID: Int64?, isJetpackConnectionPackageSupported: Bool, onCompletion: (Result<Bool, Error>) -> Void)
1515
case synchronizeSitePlan(siteID: Int64, onCompletion: (Result<Void, Error>) -> Void)
1616
case updateAccountSettings(userID: Int64, tracksOptOut: Bool, onCompletion: (Result<Void, Error>) -> Void)
17-
case removeAppleIDAccess(dotcomAppID: String, dotcomSecret: String, onCompletion: (Result<Void, Error>) -> Void)
17+
case closeAccount(onCompletion: (Result<Void, Error>) -> Void)
1818
}

0 commit comments

Comments
 (0)