Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# 3.0.2 (2025-12-02)

### Bug fixes
- Add KeychainQueryMode to prevent Keychain collisions ([781db24](https://github.com/twilio/twilio-verify-ios/commit/781db24f9bbdb181c0c86385e812bc163174a580))

Architecture | Compressed Size | Uncompressed Size
------------ | --------------- | -----------------
arm64 | 0.2 MB | 0.5 MB


# 3.0.1 (2025-10-24)

### Bug fixes
- Filter factors information to prevent getting biometrics protected data (#239) ([1d35541](https://github.com/twilio/twilio-verify-ios/commit/1d35541421e216fe6efeb98f37050a0f83406006))

### Building system
- CI update (#236) ([62e4525](https://github.com/twilio/twilio-verify-ios/commit/62e4525aa82f26d8dc1e0757dfa12df53b3bd830))
- Address Gemfile error in the pipeline ([efceb32](https://github.com/twilio/twilio-verify-ios/commit/efceb324da9fd8ddd5c11d311f1a4e0d6671adda))

Architecture | Compressed Size | Uncompressed Size
------------ | --------------- | -----------------
arm64 | 0.2 MB | 0.5 MB


# 3.0.0 (2025-04-30)

### Features
Expand Down
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* [Running the sample backend](#SampleBackend)
* [Using the sample app](#UsingSampleApp)
* [Logging](#Logging)
* [Keychain Query Mode](#KeychainQueryMode)
* [Errors](#Errors)
* [Update factor's push token](#UpdatePushToken)
* [Delete a factor](#DeleteFactor)
Expand Down Expand Up @@ -64,18 +65,18 @@ None
[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate TwilioVerify into your Xcode project using CocoaPods, specify it in your `Podfile`:

```ruby
pod 'TwilioVerify', '~> 3.0.0'
pod 'TwilioVerify', '~> 3.0.2'
```

### Carthage

[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate TwilioVerify into your Xcode project using Carthage, specify it in your `Cartfile`:

```ogdl
github "twilio/twilio-verify-ios" -> 3.0.0
github "twilio/twilio-verify-ios" -> 3.0.2
```

Since version `3.0.0` of `TwilioVerifySDK` the prebuilt asset fat version `.framework` is been deprecated, to give space for the universal framework `.xcframework`. Make sure to use the new version of Carthage [0.38.0](https://github.com/Carthage/Carthage/releases/tag/0.38.0) that was release in order to support the `xcframework` assets, by using this version or a superior one, Carthage will download and unzip the `TwilioVerifySDK.framework.zip` attached in the release version, resulting in a `TwilioVerifySDK.xcframework` that can be found in the build folder of Carthage.
Since version `3.0.2` of `TwilioVerifySDK` the prebuilt asset fat version `.framework` is been deprecated, to give space for the universal framework `.xcframework`. Make sure to use the new version of Carthage [0.38.0](https://github.com/Carthage/Carthage/releases/tag/0.38.0) that was release in order to support the `xcframework` assets, by using this version or a superior one, Carthage will download and unzip the `TwilioVerifySDK.framework.zip` attached in the release version, resulting in a `TwilioVerifySDK.xcframework` that can be found in the build folder of Carthage.

### Swift Package Manager

Expand All @@ -85,7 +86,7 @@ Once you have your Swift package set up, adding TwilioVerify as a dependency is

```swift
dependencies: [
.package(url: "https://github.com/twilio/twilio-verify-ios.git", .upToNextMajor(from: "3.0.0"))
.package(url: "https://github.com/twilio/twilio-verify-ios.git", .upToNextMajor(from: "3.0.2"))
]
```

Expand All @@ -103,7 +104,7 @@ If you want to receive challenges as push notifications, you should register You
The SDK should be used from a Swift class.
See an example in the [TwilioVerifyAdapter class](https://github.com/twilio/twilio-verify-ios/blob/main/TwilioVerifyDemo/TwilioVerifyDemo/TwilioVerify/TwilioVerifyAdapter.swift)

Since version `3.0.0`, the target was changed from `TwilioVerify` to `TwilioVerifySDK`. Migrating from older versions will imply to update all the imports in your files, see an example in the [TwilioVerifyAdapter class](https://github.com/twilio/twilio-verify-ios/blob/main/TwilioVerifyDemo/TwilioVerifyDemo/TwilioVerify/TwilioVerifyAdapter.swift#L19)
Since version `3.0.2`, the target was changed from `TwilioVerify` to `TwilioVerifySDK`. Migrating from older versions will imply to update all the imports in your files, see an example in the [TwilioVerifyAdapter class](https://github.com/twilio/twilio-verify-ios/blob/main/TwilioVerifyDemo/TwilioVerifyDemo/TwilioVerify/TwilioVerifyAdapter.swift#L19)

---

Expand Down Expand Up @@ -185,6 +186,33 @@ var builder = TwilioVerifyBuilder()
twilioVerify = try builder.build()
```

<a name='KeychainQueryMode'></a>

## Keychain Query Mode
By default, the SDK uses legacy query mode for backward compatibility with versions prior to 2.0. You can configure the Keychain query mode to control how the SDK accesses stored factors and keys.

### Available Modes
* **strict**: Recommended for new integrations. Filters Keychain items by the specific Service name (`TwilioVerify`). This isolates the SDK data and prevents collisions with keychain items from other libraries.
* **legacy**: Recommended for users upgrading from versions prior to 2.0 of Twilio Verify. Queries the Keychain without a Service filter for backward compatibility. **Warning:** May cause collisions if other keychain items exist with similar attributes.

### Usage
To set the query mode, use the `setQueryMode` method when building the `TwilioVerify` instance:

```swift
// For new integrations, use strict mode
let builder = TwilioVerifyBuilder()
let twilioVerify = try builder.setQueryMode(.strict).build()

// For upgrading from versions prior to 2.0, use legacy mode (default)
let builder = TwilioVerifyBuilder()
let twilioVerify = try builder.setQueryMode(.legacy).build()
```

### Important Notes
- The default value is `.legacy` for backward compatibility with existing integrations.
- If you're starting a new integration, it's recommended to use `.strict` mode to avoid potential Keychain collisions.
- If you're upgrading from a version prior to 2.0, you should use `.legacy` mode to ensure existing factors remain accessible.

<a name='Errors'></a>

## Errors
Expand Down
2 changes: 1 addition & 1 deletion TwilioVerify.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'TwilioVerify'
s.module_name = 'TwilioVerifySDK'
s.version = '3.0.0'
s.version = '3.0.2'
s.license = { :type => 'Apache-2.0', :file => 'LICENSE' }
s.summary = 'TwilioVerify'
s.homepage = 'https://github.com/twilio/twilio-verify-ios'
Expand Down
12 changes: 6 additions & 6 deletions TwilioVerifyDemo/TwilioVerifyDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -606,10 +606,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = TwilioVerifyDemo/TwilioVerifyDemo.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = 9EVH78F4V4;
INFOPLIST_FILE = TwilioVerifyDemo/Common/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
Expand All @@ -630,10 +630,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = TwilioVerifyDemo/TwilioVerifyDemo.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = 9EVH78F4V4;
INFOPLIST_FILE = TwilioVerifyDemo/Common/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import UIKit
import TwilioVerifySDK

protocol ChallengeDetailView: class {
protocol ChallengeDetailView: AnyObject {
func updateView()
func showAlert(withMessage message: String)
}
Expand All @@ -39,19 +39,21 @@ class ChallengeDetailViewController: UIViewController {

var presenter: ChallengeDetailPresentable?
var shouldShowButtonToDismissView = false


private var numberSelectionContainer: UIView?

override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if presenter == nil {
presenter = ChallengeDetailPresenter(withView: self)
}
}

@IBAction func updateChallenge(_ sender: UIButton) {
presenter?.updateChallenge(withStatus: sender.tag == 0 ? .denied : .approved)
}
Expand All @@ -76,10 +78,10 @@ extension ChallengeDetailViewController: ChallengeDetailView {
detailsHeightConstraint.constant = detailsTextView.contentSize.height
expirationDateLabel.text = presenter?.challenge.expirationDate.verifyStringFormat()
updatedDateLabel.text = presenter?.challenge.updatedAt.verifyStringFormat()
buttonsContainer.isHidden = !(presenter?.challenge.status == .pending)
updateButtonsVisibility()
detailsTextView.layoutSubviews()
}

func showAlert(withMessage message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Close", style: .default, handler: nil))
Expand All @@ -95,7 +97,114 @@ private extension ChallengeDetailViewController {
approveButton.layer.cornerRadius = 8
buttonsContainer.isHidden = true
}


func updateButtonsVisibility() {
numberSelectionContainer?.removeFromSuperview()
numberSelectionContainer = nil

guard presenter?.challenge.status == .pending else {
buttonsContainer.isHidden = true
return
}

if let hiddenDetails = presenter?.challenge.hiddenDetails,
let selectedNumber = hiddenDetails["selectedNumber"],
let rndNumber1 = hiddenDetails["rndNumber1"],
let rndNumber2 = hiddenDetails["rndNumber2"],
!selectedNumber.isEmpty, !rndNumber1.isEmpty, !rndNumber2.isEmpty {
buttonsContainer.isHidden = true
showNumberSelectionUI(selectedNumber: selectedNumber, numbers: [selectedNumber, rndNumber1, rndNumber2])
} else {
buttonsContainer.isHidden = false
}
}

func showNumberSelectionUI(selectedNumber: String, numbers: [String]) {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(container)

let numbersStack = UIStackView()
numbersStack.axis = .horizontal
numbersStack.distribution = .equalSpacing
numbersStack.alignment = .center
numbersStack.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(numbersStack)

for number in numbers.shuffled() {
let btn = makeNumberCircleButton(title: number)
if number == selectedNumber {
btn.addTarget(self, action: #selector(numberApproved), for: .touchUpInside)
} else {
btn.addTarget(self, action: #selector(numberDenied), for: .touchUpInside)
}
btn.addTarget(self, action: #selector(numberButtonTouchDown(_:)), for: .touchDown)
btn.addTarget(self, action: #selector(numberButtonTouchUp(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
numbersStack.addArrangedSubview(btn)
}

let declineBtn = UIButton(type: .custom)
declineBtn.translatesAutoresizingMaskIntoConstraints = false
declineBtn.setTitle("DECLINE", for: .normal)
declineBtn.backgroundColor = UIColor(red: 0.824, green: 0.133, blue: 0.176, alpha: 1)
declineBtn.setTitleColor(.white, for: .normal)
declineBtn.titleLabel?.font = .boldSystemFont(ofSize: 18)
declineBtn.layer.cornerRadius = 8
declineBtn.addTarget(self, action: #selector(numberDenied), for: .touchUpInside)
container.addSubview(declineBtn)

NSLayoutConstraint.activate([
numbersStack.topAnchor.constraint(equalTo: container.topAnchor),
numbersStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8),
numbersStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8),
numbersStack.heightAnchor.constraint(equalToConstant: 80),

declineBtn.topAnchor.constraint(equalTo: numbersStack.bottomAnchor, constant: 16),
declineBtn.leadingAnchor.constraint(equalTo: container.leadingAnchor),
declineBtn.trailingAnchor.constraint(equalTo: container.trailingAnchor),
declineBtn.heightAnchor.constraint(equalToConstant: 40),
declineBtn.bottomAnchor.constraint(equalTo: container.bottomAnchor),

container.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
container.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
container.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -48)
])

numberSelectionContainer = container
}

func makeNumberCircleButton(title: String) -> UIButton {
let btn = UIButton(type: .custom)
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle(title, for: .normal)
btn.backgroundColor = UIColor(red: 0.137, green: 0.533, blue: 0.137, alpha: 1)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .boldSystemFont(ofSize: 22)
btn.layer.cornerRadius = 40
btn.clipsToBounds = true
NSLayoutConstraint.activate([
btn.widthAnchor.constraint(equalToConstant: 80),
btn.heightAnchor.constraint(equalToConstant: 80)
])
return btn
}

@objc func numberButtonTouchDown(_ sender: UIButton) {
UIView.animate(withDuration: 0.1) { sender.alpha = 0.5 }
}

@objc func numberButtonTouchUp(_ sender: UIButton) {
UIView.animate(withDuration: 0.1) { sender.alpha = 1.0 }
}

@objc func numberApproved() {
presenter?.updateChallenge(withStatus: .approved)
}

@objc func numberDenied() {
presenter?.updateChallenge(withStatus: .denied)
}

@objc func dismissView() {
if shouldShowButtonToDismissView {
dismiss(animated: true, completion: nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ class TwilioVerifyAdapter {
init() throws {
var builder = TwilioVerifyBuilder()
#if DEBUG
builder = builder.enableDefaultLoggingService(withLevel: .all)
builder = builder
.enableDefaultLoggingService(withLevel: .all)
.setQueryMode(.strict)
#endif
twilioVerify = try builder.build()
}
Expand Down
4 changes: 4 additions & 0 deletions TwilioVerifySDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
86590DFB2DAEF16B00007F8F /* TwilioVerifySDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2AA3778246F30BE00B9388F /* TwilioVerifySDK.framework */; };
86590DFC2DAEF16B00007F8F /* TwilioVerifySDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F2AA3778246F30BE00B9388F /* TwilioVerifySDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
86649FCD27BD4A7D0006D320 /* Storage+AccessGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86649FCC27BD4A7D0006D320 /* Storage+AccessGroupTests.swift */; };
86C362622ED650A6009145B1 /* Storage+QueryMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86C362612ED650A6009145B1 /* Storage+QueryMode.swift */; };
86D6268727B5ADAB00A8B341 /* Storage+AccessGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D6268627B5ADAB00A8B341 /* Storage+AccessGroup.swift */; };
86D6268C27B5C1A200A8B341 /* StorageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D6268B27B5C1A200A8B341 /* StorageProvider.swift */; };
86D6268E27B5C22900A8B341 /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D6268D27B5C22900A8B341 /* Migration.swift */; };
Expand Down Expand Up @@ -275,6 +276,7 @@
20E5157F24A3BBC800A467FC /* ChallengeMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeMapperTests.swift; sourceTree = "<group>"; };
20EAB646248E94EC00BCD8CD /* DateFormatter+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Extensions.swift"; sourceTree = "<group>"; };
86649FCC27BD4A7D0006D320 /* Storage+AccessGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+AccessGroupTests.swift"; sourceTree = "<group>"; };
86C362612ED650A6009145B1 /* Storage+QueryMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+QueryMode.swift"; sourceTree = "<group>"; };
86D6268627B5ADAB00A8B341 /* Storage+AccessGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+AccessGroup.swift"; sourceTree = "<group>"; };
86D6268B27B5C1A200A8B341 /* StorageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageProvider.swift; sourceTree = "<group>"; };
86D6268D27B5C22900A8B341 /* Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -732,6 +734,7 @@
F207BBB124901A6900688E5C /* Storage.swift */,
86D6268B27B5C1A200A8B341 /* StorageProvider.swift */,
86D6268627B5ADAB00A8B341 /* Storage+AccessGroup.swift */,
86C362612ED650A6009145B1 /* Storage+QueryMode.swift */,
2017297724D0A3190029493D /* DateProvider.swift */,
86D6268D27B5C22900A8B341 /* Migration.swift */,
);
Expand Down Expand Up @@ -1272,6 +1275,7 @@
2003C4402480232900279BBD /* TwilioVerifyError.swift in Sources */,
20E5157D24A3B3A700A467FC /* ChallengeDTO.swift in Sources */,
20935748248719D5008FDBCC /* UpdateFactorPayload.swift in Sources */,
86C362622ED650A6009145B1 /* Storage+QueryMode.swift in Sources */,
F27A340824CF8B7200A8053D /* ECSigner.swift in Sources */,
F27A340C24CF8B7800A8053D /* ECP256SignerTemplate.swift in Sources */,
20A92FCA248197F600A9E8E7 /* HTTPMethod.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions TwilioVerifySDK/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>3.0.0</string>
<string>3.0.2</string>
<key>CFBundleVersion</key>
<string>3.0.0</string>
<string>3.0.2</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Storage+QueryMode.swift
// TwilioVerifySDK
//
// Created by Alejandro Orozco Builes on 25/11/25.
// Copyright © 2025 Twilio. All rights reserved.
//


public enum KeychainQueryMode {
/// **Recommended for new integrations.**
/// Filters Keychain items by the specific Service name (`TwilioVerify`).
/// This isolates the SDK data and prevents collisions with keychain items or other libraries.
case strict

/// **Use only for backward compatibility.**
/// Queries the Keychain without a Service filter.
/// Use this if you have users on older versions of the SDK and need to migrate their data.
/// Warning: May cause collisions if other keychain items exist with similar attributes.
case legacy
}
Loading