From ef5cd5344e139810717af4b39e119ad4faf7a939 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 19 Feb 2019 11:31:45 -0500 Subject: [PATCH] Fix the auto-format of phone numbers in the onboarding views. --- .../OnboardingPhoneNumberViewController.swift | 11 ++- .../ViewControllers/ViewControllerUtils.h | 8 ++ .../ViewControllers/ViewControllerUtils.m | 73 +++++++++++++++++-- 3 files changed, 83 insertions(+), 9 deletions(-) diff --git a/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift index 7f9d8a7a7..724fb6871 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift @@ -103,7 +103,7 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { let validationWarningRow = UIView() validationWarningRow.addSubview(validationWarningLabel) validationWarningLabel.autoPinHeightToSuperview() - validationWarningLabel.autoPinEdge(toSuperviewEdge: .leading) + validationWarningLabel.autoPinEdge(toSuperviewEdge: .trailing) let nextButton = self.button(title: NSLocalizedString("BUTTON_NEXT", comment: "Label for the 'next' button."), @@ -216,6 +216,9 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { phoneNumberTextField.isEnabled = false updateViewState() + + // Trigger the formatting logic with a no-op edit. + _ = textField(phoneNumberTextField, shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "") } // MARK: - @@ -250,6 +253,9 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { } updateViewState() + + // Trigger the formatting logic with a no-op edit. + _ = textField(phoneNumberTextField, shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "") } private func updateViewState() { @@ -365,8 +371,7 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { extension OnboardingPhoneNumberViewController: UITextFieldDelegate { public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - // TODO: Fix auto-format of phone numbers. - ViewControllerUtils.phoneNumber(textField, shouldChangeCharactersIn: range, replacementString: string, countryCode: countryCode) + ViewControllerUtils.phoneNumber(textField, shouldChangeCharactersIn: range, replacementString: string, countryCode: countryCode, prefix: callingCode) isPhoneNumberInvalid = false updateValidationWarnings() diff --git a/SignalMessaging/ViewControllers/ViewControllerUtils.h b/SignalMessaging/ViewControllers/ViewControllerUtils.h index 38b26561d..ed30d0e4c 100644 --- a/SignalMessaging/ViewControllers/ViewControllerUtils.h +++ b/SignalMessaging/ViewControllers/ViewControllerUtils.h @@ -22,6 +22,14 @@ extern NSString *const TappedStatusBarNotification; replacementString:(NSString *)insertionText countryCode:(NSString *)countryCode; +// If non-null, the prefix should represent the calling code +// prefix for the number, e.g. +1. ++ (void)phoneNumberTextField:(UITextField *)textField + shouldChangeCharactersInRange:(NSRange)range + replacementString:(NSString *)insertionText + countryCode:(NSString *)countryCode + prefix:(nullable NSString *)prefix; + + (void)ows2FAPINTextField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)insertionText; diff --git a/SignalMessaging/ViewControllers/ViewControllerUtils.m b/SignalMessaging/ViewControllers/ViewControllerUtils.m index 441e3e797..c8023e942 100644 --- a/SignalMessaging/ViewControllers/ViewControllerUtils.m +++ b/SignalMessaging/ViewControllers/ViewControllerUtils.m @@ -90,20 +90,81 @@ const NSUInteger kMax2FAPinLength = 16; // reformat the phone number, trying to keep the cursor beside the inserted or deleted digit NSUInteger cursorPositionAfterChange = MIN(left.length + center.length, textAfterChange.length); - NSString *textAfterReformat = - [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:textAfterChange - withSpecifiedCountryCodeString:countryCode]; + NSString *textToFormat = textAfterChange; + NSString *formattedText = [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:textToFormat + withSpecifiedCountryCodeString:countryCode]; NSUInteger cursorPositionAfterReformat = [PhoneNumberUtil translateCursorPosition:cursorPositionAfterChange - from:textAfterChange - to:textAfterReformat + from:textToFormat + to:formattedText stickingRightward:isJustDeletion]; - textField.text = textAfterReformat; + // PhoneNumber's formatting logic requires a calling code. + // + // If we want to edit the phone number separately from the calling code + // (e.g. in the new onboarding views), we need to temporarily prepend the + // calling code during formatting, then remove it afterward. This is + // non-trivial since the calling code itself can be affected by the + // formatting. Additionally, we need to ensure that this prepend/remove + // doesn't affect the cursor position. + BOOL hasPrefix = prefix.length > 0; + if (hasPrefix) { + // Prepend the prefix. + NSString *textToFormatWithPrefix = [prefix stringByAppendingString:textAfterChange]; + // Format with the prefix. + NSString *formattedTextWithPrefix = + [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:textToFormatWithPrefix + withSpecifiedCountryCodeString:countryCode]; + // Determine the new cursor position with the prefix. + NSUInteger cursorPositionWithPrefix = [PhoneNumberUtil translateCursorPosition:cursorPositionAfterChange + from:textToFormat + to:formattedTextWithPrefix + stickingRightward:isJustDeletion]; + // Try to determine how much of the formatted text is derived + // from the prefix. + NSString *_Nullable formattedPrefix = + [self findFormattedPrefixForPrefix:prefix formattedText:formattedTextWithPrefix]; + if (formattedPrefix && cursorPositionWithPrefix >= formattedPrefix.length) { + // Remove the prefix from the formatted text. + formattedText = [formattedTextWithPrefix substringFromIndex:formattedPrefix.length]; + // Adjust the cursor position accordingly. + cursorPositionAfterReformat = cursorPositionWithPrefix - formattedPrefix.length; + } + } + + textField.text = formattedText; UITextPosition *pos = [textField positionFromPosition:textField.beginningOfDocument offset:(NSInteger)cursorPositionAfterReformat]; [textField setSelectedTextRange:[textField textRangeFromPosition:pos toPosition:pos]]; } ++ (nullable NSString *)findFormattedPrefixForPrefix:(NSString *)prefix formattedText:(NSString *)formattedText +{ + NSCharacterSet *characterSet = [[NSCharacterSet characterSetWithCharactersInString:@"+0123456789"] invertedSet]; + NSString *filteredPrefix = + [[prefix componentsSeparatedByCharactersInSet:characterSet] componentsJoinedByString:@""]; + NSString *filteredText = + [[formattedText componentsSeparatedByCharactersInSet:characterSet] componentsJoinedByString:@""]; + if (filteredPrefix.length < 1 || filteredText.length < 1 || ![filteredText hasPrefix:filteredPrefix]) { + OWSFailDebug(@"Invalid prefix: '%@' for formatted text: '%@'", prefix, formattedText); + return nil; + } + NSString *filteredTextWithoutPrefix = [filteredText substringFromIndex:filteredPrefix.length]; + // To find the "formatted prefix", try to find the shortest "tail" of formattedText + // which after being filtered is equivalent to the "filtered text" - "filter prefix". + // The "formatted prefix" is the "head" that corresponds to that "tail". + for (NSUInteger substringLength = 1; substringLength < formattedText.length - 1; substringLength++) { + NSUInteger pivot = formattedText.length - substringLength; + NSString *head = [formattedText substringToIndex:pivot]; + NSString *tail = [formattedText substringFromIndex:pivot]; + NSString *filteredTail = + [[tail componentsSeparatedByCharactersInSet:characterSet] componentsJoinedByString:@""]; + if ([filteredTail isEqualToString:filteredTextWithoutPrefix]) { + return head; + } + } + return nil; +} + + (void)ows2FAPINTextField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)insertionText