From 673d9e0d79495eda819c459c4705b6dca6fb0d71 Mon Sep 17 00:00:00 2001 From: ramont-stripe <88752322+ramont-stripe@users.noreply.github.com> Date: Thu, 8 Sep 2022 15:58:49 -0400 Subject: [PATCH] [Link] Fix `OneTimeTextField` crash in Mac Catalyst (#1416) * Fix OneTimeTextField crash in Mac Catalyst * Update README * Fix rect calculation * Cleanup * Fix and document implementation --- CHANGELOG.md | 4 +++ Stripe/OneTimeCodeTextField.swift | 30 +++++++++++++++++---- Tests/Tests/OneTimeCodeTextFieldTests.swift | 30 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf848588cba..7df6d31ba65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## X.Y.Z +### PaymentSheet +* [Fixed] Fixed potential crash when using Link in Mac Catalyst. + ## 22.8.0 2022-09-06 ### PaymentSheet * [Changed] Renamed `PaymentSheet.reset()` to `PaymentSheet.resetCustomer()`. See `MIGRATING.md` for more info. diff --git a/Stripe/OneTimeCodeTextField.swift b/Stripe/OneTimeCodeTextField.swift index 5e408f57ce0..723776286f0 100644 --- a/Stripe/OneTimeCodeTextField.swift +++ b/Stripe/OneTimeCodeTextField.swift @@ -360,6 +360,16 @@ extension OneTimeCodeTextField: UIKeyInput { } +// MARK: - Utils + +extension OneTimeCodeTextField { + + private func clampIndex(_ index: Int) -> Int { + return max(min(index, numberOfDigits - 1), 0) + } + +} + // MARK: - UITextInput extension OneTimeCodeTextField: UITextInput { @@ -517,15 +527,25 @@ extension OneTimeCodeTextField: UITextInput { } func firstRect(for range: UITextRange) -> CGRect { - guard let range = range as? TextRange else { + guard let range = range as? TextRange, !range.isEmpty else { return .zero } - let firstDigitView = digitViews[range._start.index] - let secondDigitView = digitViews[range._end.index] + // This method should return a rectangle that contains the digit views that + // fall inside the given TextRange. For example, a [0,2] TextRange should + // return a rectangle that contains digit views 0 and 1: + // + // 0 1 2 3 4 5 6 <- TextPosition + // [*] [*] [*] [*] [*] [*] <- UI + // 0 1 2 3 4 5 <- DigitView index + // ^ ^ + // |_______| <- [0,2] TextRange + + let firstDigitView = digitViews[clampIndex(range._start.index)] + let secondDigitView = digitViews[clampIndex(range._end.index - 1)] let firstRect = firstDigitView.convert(firstDigitView.bounds, to: self) - let secondRect = firstDigitView.convert(secondDigitView.bounds, to: self) + let secondRect = secondDigitView.convert(secondDigitView.bounds, to: self) return firstRect.union(secondRect) } @@ -535,7 +555,7 @@ extension OneTimeCodeTextField: UITextInput { return .zero } - let digitView = digitViews[position.index] + let digitView = digitViews[clampIndex(position.index)] return digitView.convert(digitView.caretRect, to: self) } diff --git a/Tests/Tests/OneTimeCodeTextFieldTests.swift b/Tests/Tests/OneTimeCodeTextFieldTests.swift index 4f7208fa3e9..238ec657185 100644 --- a/Tests/Tests/OneTimeCodeTextFieldTests.swift +++ b/Tests/Tests/OneTimeCodeTextFieldTests.swift @@ -236,6 +236,36 @@ class OneTimeCodeTextFieldTests: XCTestCase { XCTAssertNil(field.characterRange(byExtending: position, in: .down)) } + func test_firstRectForRange_singleDigit() { + let sut = makeSUT(value: "123456") + + // A [0,1] text range + let range = OneTimeCodeTextField.TextRange( + start: OneTimeCodeTextField.TextPosition(0), + end: OneTimeCodeTextField.TextPosition(1) + ) + let rect = sut.firstRect(for: range) + XCTAssertEqual(rect.minX, 0, accuracy: 0.2) + XCTAssertEqual(rect.minY, 0, accuracy: 0.2) + XCTAssertEqual(rect.width, 46.0, accuracy: 0.2) + XCTAssertEqual(rect.height, 60, accuracy: 0.2) + } + + func test_firstRectForRange_multipleDigits() { + let sut = makeSUT(value: "123456") + + // A [0,3] Text range + let range = OneTimeCodeTextField.TextRange( + start: OneTimeCodeTextField.TextPosition(0), + end: OneTimeCodeTextField.TextPosition(3) + ) + let rect = sut.firstRect(for: range) + XCTAssertEqual(rect.minX, 0, accuracy: 0.2) + XCTAssertEqual(rect.minY, 0, accuracy: 0.2) + XCTAssertEqual(rect.width, 150, accuracy: 0.2) + XCTAssertEqual(rect.height, 60, accuracy: 0.2) + } + func test_caretRectForPosition() { let sut = makeSUT() let frame = sut.caretRect(for: OneTimeCodeTextField.TextPosition(1))