summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMojgan <Mojgan.jelodar@codic.se>2023-06-07 16:11:59 +0200
committerMojgan <Mojgan.jelodar@codic.se>2023-06-08 19:21:09 +0200
commit91b480d0c5b3562c715bcfa831ea1459415a580b (patch)
treef404183aa44c93c795216bc4e0f3a6765ef18c2a
parente204ff2565a99058cf9a79f69ddc14d82a1b531d (diff)
downloadmullvadvpn-91b480d0c5b3562c715bcfa831ea1459415a580b.tar.xz
mullvadvpn-91b480d0c5b3562c715bcfa831ea1459415a580b.zip
Format text based on group size via separator
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj41
-rw-r--r--ios/MullvadVPN/Classes/InputTextFormatter.swift (renamed from ios/MullvadVPN/View controllers/Login/AccountTokenInput.swift)107
-rw-r--r--ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift3
-rw-r--r--ios/MullvadVPN/View controllers/Login/AccountTextField.swift19
-rw-r--r--ios/MullvadVPNTests/AccountTokenInputTests.swift132
-rw-r--r--ios/MullvadVPNTests/InputTextFormatterTests.swift157
6 files changed, 262 insertions, 197 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 85f36e1a15..73a534a971 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -84,8 +84,7 @@
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; };
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; };
582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */; };
- 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; };
- 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */; };
+ 582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */; };
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1AE229566420055B6EF /* SettingsCell.swift */; };
582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */; };
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; };
@@ -240,7 +239,7 @@
58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E272943527300D5980C /* SystemNotificationProvider.swift */; };
58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E292943545A00D5980C /* NotificationManagerDelegate.swift */; };
58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* TunnelControlView.swift */; };
- 58B8644529C7971B005E107C /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; };
+ 58B8644529C7971B005E107C /* InputTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */; };
58B8644629C7972F005E107C /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; };
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B93A1226C3F13600A55733 /* TunnelState.swift */; };
58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B993B02608A34500BA7811 /* LoginContentView.swift */; };
@@ -403,6 +402,7 @@
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */; };
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; };
+ F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */; };
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; };
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; };
/* End PBXBuildFile section */
@@ -847,8 +847,7 @@
58293FB025124117005D0BB5 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
58293FB2251241B3005D0BB5 /* CustomTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextView.swift; sourceTree = "<group>"; };
582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedWidthIntegerArithmeticsTests.swift; sourceTree = "<group>"; };
- 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountTokenInput.swift; sourceTree = "<group>"; };
- 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTokenInputTests.swift; sourceTree = "<group>"; };
+ 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatter.swift; sourceTree = "<group>"; };
582BB1AE229566420055B6EF /* SettingsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; };
582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Appearance.swift"; sourceTree = "<group>"; };
582FFA82290A84E700895745 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -1115,6 +1114,7 @@
E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; };
E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = "<group>"; };
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; };
+ F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = "<group>"; };
F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = "<group>"; };
F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -1426,19 +1426,19 @@
583FE01629C196E8006E85F9 /* View controllers */ = {
isa = PBXGroup;
children = (
- 583FE02529C1AD0E006E85F9 /* Launch */,
- 583FE02229C1AC68006E85F9 /* TermsOfService */,
- 583FE02129C1A0F4006E85F9 /* Login */,
583FE02029C1A0B1006E85F9 /* Account */,
- 583FE01E29C197D5006E85F9 /* Tunnel */,
+ 5878F4FA29CDA2D4003D4BE2 /* ChangeLog */,
583FE01D29C197C1006E85F9 /* DeviceList */,
- 583FE01C29C19793006E85F9 /* RevokedDevice */,
+ 583FE02529C1AD0E006E85F9 /* Launch */,
+ 583FE02129C1A0F4006E85F9 /* Login */,
583FE01B29C19786006E85F9 /* OutOfTime */,
- 5878F4FA29CDA2D4003D4BE2 /* ChangeLog */,
583FE01A29C19777006E85F9 /* Preferences */,
583FE01929C19760006E85F9 /* ProblemReport */,
- 583FE01829C19709006E85F9 /* Settings */,
+ 583FE01C29C19793006E85F9 /* RevokedDevice */,
583FE01729C196F3006E85F9 /* SelectLocation */,
+ 583FE01829C19709006E85F9 /* Settings */,
+ 583FE02229C1AC68006E85F9 /* TermsOfService */,
+ 583FE01E29C197D5006E85F9 /* Tunnel */,
);
path = "View controllers";
sourceTree = "<group>";
@@ -1577,12 +1577,11 @@
583FE02129C1A0F4006E85F9 /* Login */ = {
isa = PBXGroup;
children = (
- 58B993B02608A34500BA7811 /* LoginContentView.swift */,
- 58CE5E65224146200008646E /* LoginViewController.swift */,
- 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */,
- 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */,
58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */,
58CCA01D2242787B004F3011 /* AccountTextField.swift */,
+ 58B993B02608A34500BA7811 /* LoginContentView.swift */,
+ 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */,
+ 58CE5E65224146200008646E /* LoginViewController.swift */,
);
path = Login;
sourceTree = "<group>";
@@ -1662,6 +1661,7 @@
5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */,
58138E60294871C600684F0C /* DeviceDataThrottling.swift */,
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */,
+ 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */,
58CC40EE24A601900019D96E /* ObserverList.swift */,
5872D6E7286304DE00DB5F4E /* TermsOfService.swift */,
);
@@ -1854,11 +1854,11 @@
58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = {
isa = PBXGroup;
children = (
- 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */,
5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */,
58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */,
582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */,
58B0A2A4238EE67E00BC001D /* Info.plist */,
+ F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */,
584B26F3237434D00073B10E /* RelaySelectorTests.swift */,
5807E2C1243203D000F5FF30 /* StringTests.swift */,
58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */,
@@ -2838,12 +2838,12 @@
buildActionMask = 2147483647;
files = (
58915D6A2A26031B0066445B /* DNSSettings.swift in Sources */,
- 58B8644529C7971B005E107C /* AccountTokenInput.swift in Sources */,
+ 58B8644529C7971B005E107C /* InputTextFormatter.swift in Sources */,
58915D692A2601FB0066445B /* WgKeyRotation.swift in Sources */,
- 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */,
580810E62A30E13D00B74552 /* DeviceStateAccessorProtocol.swift in Sources */,
58915D642A25F8B30066445B /* DeviceCheckOperation.swift in Sources */,
58915D652A25F9E20066445B /* TunnelSettingsV2.swift in Sources */,
+ 58B8644529C7971B005E107C /* InputTextFormatter.swift in Sources */,
582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */,
58915D632A25F8400066445B /* DeviceCheckOperationTests.swift in Sources */,
5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */,
@@ -2852,6 +2852,7 @@
58165EBE2A262CBB00688EAD /* WgKeyRotationTests.swift in Sources */,
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */,
580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */,
+ F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */,
58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2933,7 +2934,7 @@
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */,
5878A279290954790096FC88 /* TunnelViewControllerInteractor.swift in Sources */,
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */,
- 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */,
+ 582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */,
5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */,
5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */,
58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */,
diff --git a/ios/MullvadVPN/View controllers/Login/AccountTokenInput.swift b/ios/MullvadVPN/Classes/InputTextFormatter.swift
index 1fef51a479..89e1df7607 100644
--- a/ios/MullvadVPN/View controllers/Login/AccountTokenInput.swift
+++ b/ios/MullvadVPN/Classes/InputTextFormatter.swift
@@ -1,5 +1,5 @@
//
-// AccountTokenInput.swift
+// InputTextFormatter.swift
// MullvadVPN
//
// Created by pronebird on 08/04/2020.
@@ -11,15 +11,33 @@ import UIKit
/// A class describing the account token input and caret management.
/// Suitable to be used with `UITextField`.
-class AccountTokenInput: NSObject {
- /// The group separator character
- static let groupSeparator: Character = " "
+class InputTextFormatter: NSObject, UITextFieldDelegate, UITextPasteDelegate {
+ enum AllowedInput {
+ case numeric, alphanumeric(isUpperCase: Bool)
+ }
+
+ struct Configuration {
+ /// Allowed characters.
+ var allowedInput: AllowedInput
+
+ /// Separator between groups of characters.
+ var groupSeparator: Character
- /// The character size of each group of digits
- static let groupSize = 4
+ /// The size of each group of characters .
+ var groupSize: UInt8
- /// Parsed account token string
- private(set) var parsedString = ""
+ /// Maximum number of groups of characters allowed.
+ var maxGroups: UInt
+ }
+
+ var configuration: Configuration {
+ didSet {
+ replace(with: string)
+ }
+ }
+
+ /// Parsed string
+ private(set) var string = ""
/// Formatted string
private(set) var formattedString = ""
@@ -27,7 +45,8 @@ class AccountTokenInput: NSObject {
// Computed caret position
private(set) var caretPosition = 0
- init(string: String = "") {
+ init(string: String = "", configuration: Configuration) {
+ self.configuration = configuration
super.init()
replace(with: string)
@@ -45,13 +64,8 @@ class AccountTokenInput: NSObject {
}
/// Replace characters in range maintaining the caret position
- ///
- /// - Parameter range: a range within a string to replace
- /// - Parameter replacementString: a string to replace the characters in the given range
- /// - Parameter emptySelection: a hint to indicate if the text field selection is empty.
- /// This is normally the default state unless a text range is
- /// selected.
- ///
+ /// Note: `emptySelection` hints that text field selection is empty. This is normally
+ /// the default state unless a text range is selected.
func replaceCharacters(
in range: Range<String.Index>,
replacementString: String,
@@ -61,10 +75,12 @@ class AccountTokenInput: NSObject {
// Since removing separator alone makes no sense, this computation extends the string range
// to include the digit preceding a separator.
- if replacementString.isEmpty, emptySelection, !formattedString.isEmpty {
+ if replacementString.isEmpty, emptySelection,
+ !formattedString.isEmpty
+ {
let precedingDigitIndex = formattedString
.prefix(through: stringRange.lowerBound)
- .lastIndex { Self.isDigit($0) } ?? formattedString.startIndex
+ .lastIndex { isAllowed($0) } ?? formattedString.startIndex
stringRange = precedingDigitIndex ..< stringRange.upperBound
}
@@ -96,7 +112,7 @@ class AccountTokenInput: NSObject {
for (index, element) in newString.enumerated() {
// Skip disallowed characters
- if !Self.isDigit(element) {
+ if !isAllowed(element) {
// Adjust the caret position for characters removed before the insertion location
if originalCaretPosition > index {
newCaretPosition -= 1
@@ -104,9 +120,24 @@ class AccountTokenInput: NSObject {
continue
}
+ // Apply cap on number of groups of characters that can be entered.
+ if configuration.maxGroups > 0 {
+ let numGroups = reparsedString.count / Int(configuration.groupSize)
+
+ if numGroups >= configuration.maxGroups {
+ if originalCaretPosition > index {
+ newCaretPosition = reformattedString.count
+ }
+ break
+ }
+ }
+
// Add separator between the groups of digits
- if numDigits > 0, numDigits % Self.groupSize == 0 {
- reformattedString.append(Self.groupSeparator)
+ if numDigits > 0,
+ configuration.groupSize > 0,
+ numDigits % Int(configuration.groupSize) == 0
+ {
+ reformattedString.append(configuration.groupSeparator)
if originalCaretPosition > index {
// Adjust the caret position to account for separators added before the
@@ -120,22 +151,15 @@ class AccountTokenInput: NSObject {
numDigits += 1
}
+ if case AllowedInput.alphanumeric(true) = configuration.allowedInput {
+ reformattedString = reformattedString.uppercased()
+ }
+
caretPosition = newCaretPosition
formattedString = reformattedString
- parsedString = reparsedString
+ string = reparsedString
}
- private class func isDigit(_ character: Character) -> Bool {
- switch character {
- case "0" ... "9":
- return true
- default:
- return false
- }
- }
-}
-
-extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate {
/// Update the text and caret position in the given text field
func updateTextField(_ textField: UITextField) {
updateTextField(textField, notifyDelegate: false)
@@ -197,7 +221,7 @@ extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate {
// MARK: - Private
- /// A caret position as utf-16 offset compatible for use with `NSString` and `UITextField`
+ /// A caret position as utf-16 offset compatible for use with `NSString` and `UITextField`.
private var caretPositionUtf16: Int {
let startIndex = formattedString.startIndex
let endIndex = formattedString.index(startIndex, offsetBy: caretPosition)
@@ -205,7 +229,7 @@ extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate {
return formattedString.utf16.distance(from: startIndex, to: endIndex)
}
- /// Convert the computed caret position to an empty `UITextRange` within the given text field
+ /// Convert the computed caret position to an empty `UITextRange` within the given text field.
private func caretTextRange(in textField: UITextField) -> UITextRange? {
guard let position = textField.position(
from: textField.beginningOfDocument,
@@ -216,7 +240,7 @@ extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate {
}
/// A helper to update the text and caret in the given text field, and optionally post
- /// `UITextField.textDidChange` notification
+ /// `UITextField.textDidChange` notification.
private func updateTextField(_ textField: UITextField, notifyDelegate: Bool) {
textField.text = formattedString
textField.selectedTextRange = caretTextRange(in: textField)
@@ -226,11 +250,20 @@ extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate {
}
}
- /// Post `UITextField.textDidChange` notification
+ /// Posts `UITextField.textDidChange` notification.
private class func notifyTextDidChange(in textField: UITextField) {
NotificationCenter.default.post(
name: UITextField.textDidChangeNotification,
object: textField
)
}
+
+ private func isAllowed(_ character: Character) -> Bool {
+ switch configuration.allowedInput {
+ case .numeric:
+ return character.isASCII && character.isNumber
+ case .alphanumeric:
+ return character.isASCII && character.isLetter
+ }
+ }
}
diff --git a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift
index 4ea71c6b9c..ec298656ca 100644
--- a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift
+++ b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift
@@ -9,9 +9,10 @@
import UIKit
private let animationDuration: TimeInterval = 0.25
-private let minimumAccountTokenLength = 10
final class AccountInputGroupView: UIView {
+ private let minimumAccountTokenLength = 10
+
enum Style {
case normal, error, authenticating
}
diff --git a/ios/MullvadVPN/View controllers/Login/AccountTextField.swift b/ios/MullvadVPN/View controllers/Login/AccountTextField.swift
index 74c6c1c45e..cf3412ab6c 100644
--- a/ios/MullvadVPN/View controllers/Login/AccountTextField.swift
+++ b/ios/MullvadVPN/View controllers/Login/AccountTextField.swift
@@ -9,7 +9,12 @@
import UIKit
class AccountTextField: CustomTextField, UITextFieldDelegate {
- private let input = AccountTokenInput()
+ private let inputFormatter = InputTextFormatter(configuration: InputTextFormatter.Configuration(
+ allowedInput: .numeric,
+ groupSeparator: " ",
+ groupSize: 4,
+ maxGroups: 4
+ ))
var onReturnKey: ((AccountTextField) -> Bool)?
@@ -20,7 +25,7 @@ class AccountTextField: CustomTextField, UITextFieldDelegate {
cornerRadius = 0
delegate = self
- pasteDelegate = input
+ pasteDelegate = inputFormatter
NotificationCenter.default.addObserver(
self,
@@ -36,16 +41,16 @@ class AccountTextField: CustomTextField, UITextFieldDelegate {
var autoformattingText: String {
set {
- input.replace(with: newValue)
- input.updateTextField(self)
+ inputFormatter.replace(with: newValue)
+ inputFormatter.updateTextField(self)
}
get {
- input.formattedString
+ inputFormatter.formattedString
}
}
var parsedToken: String {
- return input.parsedString
+ return inputFormatter.string
}
var enableReturnKey = true {
@@ -70,7 +75,7 @@ class AccountTextField: CustomTextField, UITextFieldDelegate {
shouldChangeCharactersIn range: NSRange,
replacementString string: String
) -> Bool {
- return input.textField(
+ return inputFormatter.textField(
textField,
shouldChangeCharactersIn: range,
replacementString: string
diff --git a/ios/MullvadVPNTests/AccountTokenInputTests.swift b/ios/MullvadVPNTests/AccountTokenInputTests.swift
deleted file mode 100644
index 82a09e1285..0000000000
--- a/ios/MullvadVPNTests/AccountTokenInputTests.swift
+++ /dev/null
@@ -1,132 +0,0 @@
-//
-// AccountTokenInputTests.swift
-// MullvadVPNTests
-//
-// Created by pronebird on 10/04/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import XCTest
-
-private let kSampleToken = "12345678"
-
-class AccountTokenInputTests: XCTestCase {
- func testInitialValue() {
- let input = AccountTokenInput(string: kSampleToken)
-
- XCTAssertEqual(input.formattedString, "1234 5678")
- XCTAssertEqual(input.caretPosition, 9)
- }
-
- func testReplacingValue() {
- let input = AccountTokenInput()
- input.replace(with: "00000000")
-
- XCTAssertEqual(input.formattedString, "0000 0000")
- XCTAssertEqual(input.caretPosition, 9)
- }
-
- func testRemovingSeparator() {
- let input = AccountTokenInput(string: kSampleToken)
-
- input.replaceCharacters(
- in: input.formattedString.range(withOffset: 4, length: 1),
- replacementString: "",
- emptySelection: true
- )
-
- XCTAssertEqual(input.formattedString, "1235 678")
- XCTAssertEqual(input.caretPosition, 3)
- }
-
- func testRemovingSeparatorRange() {
- let input = AccountTokenInput(string: kSampleToken)
-
- input.replaceCharacters(
- in: input.formattedString.range(withOffset: 4, length: 1),
- replacementString: "",
- emptySelection: false
- )
-
- XCTAssertEqual(input.formattedString, "1234 5678")
- XCTAssertEqual(input.caretPosition, 4)
- }
-
- func testRemovingRange() {
- let input = AccountTokenInput(string: kSampleToken)
-
- input.replaceCharacters(
- in: input.formattedString.range(withOffset: 7, length: 2),
- replacementString: "",
- emptySelection: false
- )
-
- XCTAssertEqual(input.formattedString, "1234 56")
- XCTAssertEqual(input.caretPosition, 7)
- }
-
- func testInserting() {
- let input = AccountTokenInput(string: kSampleToken)
-
- input.replaceCharacters(
- in: input.formattedString.range(withOffset: 5, length: 0),
- replacementString: "0000",
- emptySelection: true
- )
-
- XCTAssertEqual(input.formattedString, "1234 0000 5678")
- XCTAssertEqual(input.caretPosition, 9)
- }
-
- func testReplacingRange() {
- let input = AccountTokenInput(string: kSampleToken)
-
- input.replaceCharacters(
- in: input.formattedString.range(withOffset: 5, length: 4),
- replacementString: "0000",
- emptySelection: false
- )
-
- XCTAssertEqual(input.formattedString, "1234 0000")
- XCTAssertEqual(input.caretPosition, 9)
- }
-
- func testInvalidCharactersReplacesTextFieldTextWithFormattedString() {
- let input = AccountTokenInput(string: kSampleToken)
- let invalidRange = NSRange(location: kSampleToken.count + 1, length: 0)
- let textField = UITextField()
-
- _ = input.textField(textField, shouldChangeCharactersIn: invalidRange, replacementString: "´")
-
- XCTAssertEqual(textField.text, input.formattedString)
- }
-
- func testDeleteCharacterOutsideOfTokenBoundaryDoesNotDeleteAnything() {
- let input = AccountTokenInput(string: kSampleToken)
- let invalidRange = NSRange(location: kSampleToken.count + 1, length: 1)
- let textField = UITextField()
-
- _ = input.textField(textField, shouldChangeCharactersIn: invalidRange, replacementString: "")
-
- XCTAssertEqual(textField.text, input.formattedString)
- }
-
- func testDeleteLastCharacter() {
- let input = AccountTokenInput(string: kSampleToken)
- let lastCharacterRange = NSRange(location: kSampleToken.count, length: 1)
- let textField = UITextField()
-
- _ = input.textField(textField, shouldChangeCharactersIn: lastCharacterRange, replacementString: "")
-
- XCTAssertEqual(textField.text, "1234 567")
- }
-}
-
-private extension String {
- func range(withOffset offset: Int, length: Int) -> Range<String.Index> {
- let start = index(startIndex, offsetBy: offset)
- let end = index(start, offsetBy: length)
-
- return start ..< end
- }
-}
diff --git a/ios/MullvadVPNTests/InputTextFormatterTests.swift b/ios/MullvadVPNTests/InputTextFormatterTests.swift
new file mode 100644
index 0000000000..5ff8d0daef
--- /dev/null
+++ b/ios/MullvadVPNTests/InputTextFormatterTests.swift
@@ -0,0 +1,157 @@
+//
+// InputTextFormatterTests.swift
+// MullvadVPNTests
+//
+// Created by pronebird on 10/04/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import XCTest
+
+class InputTextFormatterTests: XCTestCase {
+ private let accountNumber = "12345678"
+ private var inputTextFormatter: InputTextFormatter!
+
+ private let configuration = InputTextFormatter.Configuration(
+ allowedInput: .numeric,
+ groupSeparator: " ",
+ groupSize: 4,
+ maxGroups: 4
+ )
+
+ override func setUp() {
+ inputTextFormatter = InputTextFormatter(
+ string: accountNumber,
+ configuration: configuration
+ )
+ }
+
+ override func tearDown() {
+ inputTextFormatter = nil
+ }
+
+ func testInitialValue() {
+ XCTAssertEqual(inputTextFormatter.formattedString, "1234 5678")
+ XCTAssertEqual(inputTextFormatter.caretPosition, 9)
+ }
+
+ func testReplacingValue() {
+ inputTextFormatter.replace(with: "00000000")
+
+ XCTAssertEqual(inputTextFormatter.formattedString, "0000 0000")
+ XCTAssertEqual(inputTextFormatter.caretPosition, 9)
+ }
+
+ func testRemovingSeparator() {
+ guard let range = inputTextFormatter.formattedString.range(withOffset: 4, length: 1) else {
+ return XCTAssertNil("Out of range", file: #file, line: #line)
+ }
+
+ inputTextFormatter.replaceCharacters(
+ in: range,
+ replacementString: "",
+ emptySelection: true
+ )
+
+ XCTAssertEqual(inputTextFormatter.formattedString, "1235 678")
+ XCTAssertEqual(inputTextFormatter.caretPosition, 3)
+ }
+
+ func testRemovingSeparatorRange() {
+ guard let range = inputTextFormatter.formattedString.range(withOffset: 4, length: 1) else {
+ return XCTAssertNil("Out of range", file: #file, line: #line)
+ }
+
+ inputTextFormatter.replaceCharacters(
+ in: range,
+ replacementString: "",
+ emptySelection: false
+ )
+
+ XCTAssertEqual(inputTextFormatter.formattedString, "1234 5678")
+ XCTAssertEqual(inputTextFormatter.caretPosition, 4)
+ }
+
+ func testRemovingRange() {
+ guard let range = inputTextFormatter.formattedString.range(withOffset: 7, length: 2) else {
+ return XCTAssertNil("Out of range", file: #file, line: #line)
+ }
+
+ inputTextFormatter.replaceCharacters(
+ in: range,
+ replacementString: "",
+ emptySelection: false
+ )
+
+ XCTAssertEqual(inputTextFormatter.formattedString, "1234 56")
+ XCTAssertEqual(inputTextFormatter.caretPosition, 7)
+ }
+
+ func testInserting() {
+ guard let range = inputTextFormatter.formattedString.range(withOffset: 5, length: 0) else {
+ return XCTAssertNil("Out of range", file: #file, line: #line)
+ }
+
+ inputTextFormatter.replaceCharacters(
+ in: range,
+ replacementString: "0000",
+ emptySelection: true
+ )
+
+ XCTAssertEqual(inputTextFormatter.formattedString, "1234 0000 5678")
+ XCTAssertEqual(inputTextFormatter.caretPosition, 9)
+ }
+
+ func testReplacingRange() {
+ guard let range = inputTextFormatter.formattedString.range(withOffset: 5, length: 4) else {
+ return XCTAssertNil("Out of range", file: #file, line: #line)
+ }
+
+ inputTextFormatter.replaceCharacters(
+ in: range,
+ replacementString: "0000",
+ emptySelection: false
+ )
+
+ XCTAssertEqual(inputTextFormatter.formattedString, "1234 0000")
+ XCTAssertEqual(inputTextFormatter.caretPosition, 9)
+ }
+
+ func testInvalidCharactersReplacesTextFieldTextWithFormattedString() {
+ let invalidRange = NSRange(location: accountNumber.count + 1, length: 0)
+ let textField = UITextField()
+
+ _ = inputTextFormatter.textField(textField, shouldChangeCharactersIn: invalidRange, replacementString: "´")
+
+ XCTAssertEqual(textField.text, inputTextFormatter.formattedString)
+ }
+
+ func testDeleteCharacterOutsideOfTokenBoundaryDoesNotDeleteAnything() {
+ let invalidRange = NSRange(location: accountNumber.count + 1, length: 1)
+ let textField = UITextField()
+
+ _ = inputTextFormatter.textField(textField, shouldChangeCharactersIn: invalidRange, replacementString: "")
+
+ XCTAssertEqual(textField.text, inputTextFormatter.formattedString)
+ }
+
+ func testDeleteLastCharacter() {
+ let lastCharacterRange = NSRange(location: accountNumber.count, length: 1)
+ let textField = UITextField()
+
+ _ = inputTextFormatter.textField(textField, shouldChangeCharactersIn: lastCharacterRange, replacementString: "")
+
+ XCTAssertEqual(textField.text, "1234 567")
+ }
+}
+
+private extension String {
+ func range(withOffset offset: Int, length: Int) -> Range<String.Index>? {
+ guard let start = index(startIndex, offsetBy: offset, limitedBy: endIndex),
+ let end = index(start, offsetBy: length, limitedBy: endIndex)
+ else {
+ return nil
+ }
+ return start ..< end
+ }
+}