From 35fee30355cccd04036a82fb123ad6e401178ae3 Mon Sep 17 00:00:00 2001 From: William Laverty Date: Wed, 4 Feb 2026 04:09:25 -0800 Subject: [PATCH] Fix SMS autofill by using single text field The previous implementation used 6 separate text fields for the SMS verification code, which broke macOS's built-in SMS autofill functionality. When the system detects an SMS code and offers to autofill it, it expects a single text field to receive the entire code. Changes: - Replaced 6 individual PinCodeCharacterTextField instances with a single NSTextField - Added .oneTimeCode content type to enable SMS autofill on macOS 11+ - Simplified the code structure while maintaining the same functionality - Added input validation to ensure only digits are accepted Now when users receive an SMS verification code, macOS can properly autofill the entire code with a single click, rather than filling only the first digit and deleting the message. Closes #788 --- Xcodes/Frontend/SignIn/PinCodeTextView.swift | 219 ++++++------------- 1 file changed, 72 insertions(+), 147 deletions(-) diff --git a/Xcodes/Frontend/SignIn/PinCodeTextView.swift b/Xcodes/Frontend/SignIn/PinCodeTextView.swift index f0b1c2a0..2df36110 100644 --- a/Xcodes/Frontend/SignIn/PinCodeTextView.swift +++ b/Xcodes/Frontend/SignIn/PinCodeTextView.swift @@ -9,29 +9,24 @@ struct PinCodeTextField: NSViewRepresentable { let complete: (String) -> Void func makeNSView(context: Context) -> NSViewType { - let view = PinCodeTextView(numberOfDigits: numberOfDigits, itemSpacing: 10) + let view = PinCodeTextView(numberOfDigits: numberOfDigits) view.codeDidChange = { c in code = c } view.codeDidComplete = { complete($0) } return view } func updateNSView(_ nsView: NSViewType, context: Context) { - nsView.code = (0.. Void)? = nil private let numberOfDigits: Int - private let stackView: NSStackView = .init(frame: .zero) - private var characterViews: [PinCodeCharacterTextField] = [] - + private let textField: NSTextField + // MARK: - Initializers - init( - numberOfDigits: Int, - itemSpacing: CGFloat - ) { + init(numberOfDigits: Int) { self.numberOfDigits = numberOfDigits + self.textField = NSTextField(frame: .zero) + super.init(frame: .zero) - - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.spacing = itemSpacing - stackView.orientation = .horizontal - stackView.distribution = .fillEqually - stackView.alignment = .centerY - addSubview(stackView) - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: self.topAnchor), - stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - stackView.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor), - stackView.trailingAnchor.constraint(greaterThanOrEqualTo: self.trailingAnchor), - stackView.centerXAnchor.constraint(equalTo: self.centerXAnchor), - ]) - self.code = (0.. Bool { - if commandSelector == #selector(deleteBackward(_:)) { - // If empty, move to previous or first character view - if textView.string.isEmpty { - if let lastFieldIndexWithCharacter = code.lastIndex(where: { $0 != nil }) { - window?.makeFirstResponder(characterViews[lastFieldIndexWithCharacter]) - } else { - window?.makeFirstResponder(characterViews[0]) - } - - return true - } - } - - // Perform default behaviour - return false + private func setupLayout() { + NSLayoutConstraint.activate([ + textField.topAnchor.constraint(equalTo: topAnchor), + textField.bottomAnchor.constraint(equalTo: bottomAnchor), + textField.leadingAnchor.constraint(equalTo: leadingAnchor), + textField.trailingAnchor.constraint(equalTo: trailingAnchor), + textField.widthAnchor.constraint(greaterThanOrEqualToConstant: CGFloat(numberOfDigits * 30 + 40)), + textField.heightAnchor.constraint(greaterThanOrEqualToConstant: 50) + ]) } + // MARK: NSTextFieldDelegate + func controlTextDidChange(_ obj: Notification) { guard let field = obj.object as? NSTextField, - isEnabled, - let fieldIndex = characterViews.firstIndex(where: { $0 === field }) - else { return } + field === textField, + isEnabled + else { return } - let newFieldText = field.stringValue + let newText = field.stringValue - let lastCharacter: Character? - if newFieldText.isEmpty { - lastCharacter = nil - } else { - lastCharacter = newFieldText[newFieldText.index(before: newFieldText.endIndex)] - } - - code[fieldIndex] = lastCharacter + // Filter to only digits + let filteredText = String(newText.filter { $0.isNumber }.prefix(numberOfDigits)) - if lastCharacter != nil { - if fieldIndex >= characterViews.count - 1 { - resignFirstResponder() - } else { - window?.makeFirstResponder(characterViews[fieldIndex + 1]) - } - } else { - if let lastFieldIndexWithCharacter = code.lastIndex(where: { $0 != nil }) { - window?.makeFirstResponder(characterViews[lastFieldIndexWithCharacter]) - } else { - window?.makeFirstResponder(characterViews[0]) - } + if filteredText != newText { + field.stringValue = filteredText } + + code = filteredText } // MARK: NSResponder @@ -180,52 +151,6 @@ class PinCodeTextView: NSControl, NSTextFieldDelegate { } override func becomeFirstResponder() -> Bool { - characterViews.first?.becomeFirstResponder() ?? false - } -} - -// MARK: - PinCodeCharacterTextField - -class PinCodeCharacterTextField: NSTextField { - var character: Character? = nil { - didSet { - stringValue = character.map(String.init) ?? "" - } - } - private var lastSize: NSSize? - - init() { - super.init(frame: .zero) - - wantsLayer = true - alignment = .center - maximumNumberOfLines = 1 - font = .boldSystemFont(ofSize: 48) - - setContentHuggingPriority(.required, for: .vertical) - setContentHuggingPriority(.required, for: .horizontal) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func textDidChange(_ notification: Notification) { - super.textDidChange(notification) - self.invalidateIntrinsicContentSize() - } - - // This is kinda cheating - // Assuming that 0 is the widest and tallest character in 0-9 - override var intrinsicContentSize: NSSize { - var size = NSAttributedString( - string: "0", - attributes: [ .font : self.font! ] - ) - .size() - // I guess the cell should probably be doing this sizing in order to take into account everything outside of simply the text's frame, but for some reason I can't find a way to do that which works... - size.width += 16 - size.height += 8 - return size + textField.becomeFirstResponder() } }