Skip to content

Feature/mfa integration - #161

Open
gioraping wants to merge 47 commits into
ForgeRock:developfrom
gioraping:feature/mfa-integration
Open

Feature/mfa integration#161
gioraping wants to merge 47 commits into
ForgeRock:developfrom
gioraping:feature/mfa-integration

Conversation

@gioraping

@gioraping gioraping commented May 27, 2026

Copy link
Copy Markdown

P14C-82521

JIRA "Add PingOneMFA module to the orchestratio SDK on iOS"

Description

  • New PingOneMFA module — a clean async/await wrapper around the binary PingOneSDK (PingOneSDK XCFramework). Bridges all callback-based PingOneSDK APIs to async throws functions via withCheckedThrowingContinuation, using a @globalActor (PingOneMFAActor) for thread-safe state. Exposed surface: initialize(geo:), setDeviceToken(_:), pair(pairingKey:), getDeviceInfo(), getOneTimePasscode(), processRemoteNotification(userInfo:), processRemoteNotificationAction(identifier:authenticationMethod:userInfo:), generateMobilePayload(), getNotificationCategories().
  • Actionable push notifications — registers UNNotificationCategory objects from PingOneSDK so Approve/Deny banner actions work without the app being foregrounded; routes UNUserNotificationCenterDelegate actions through processRemoteNotificationAction.
  • PingExample sample app integration — adds a full MFA section: QR-code scanner for device pairing, paired accounts list, live-countdown OTP screen, push notification modal (approve/deny with number-matching support), and mobile payload view.
    https://pingidentity.atlassian.net/browse/P14C-86659
  • Unit tests — AccountParserTests, PingOneMFATests, PingOneMFAErrorTests, and a MockPingOneMFA covering initialization, pairing, token registration, OTP, push processing, and error bridging.
    Package.swift / Package.resolved updates — adds PingOneMFA as a new library target and pulls in the PingOneSDK binary dependency.

Testing

Set-up native application in PingOne environment

Pairing: tap QR Code Registration → scan a PingOne pairing QR → success dialog → accounts list shows the paired account
OTP: tap One-Time Passcode → code displays with countdown; refreshes automatically at zero
Payload: tap Mobile Payload → payload shown; Copy button writes to clipboard
Push (foreground): trigger a push from PingOne while app is open → approval screen appears; approve/deny shows success dialog
Push (background): trigger a push while app is backgrounded → system banner with Allow/Deny buttons → tapping either completes the request
Cancellation: approve on a second device while the approval screen is open → screen dismisses automatically

Checklist:

  • I ran all unit tests and they pass
  • I added test case coverage for my changes

Summary by CodeRabbit

  • New Features
    • Added PingOne MFA support for device pairing, account management, one-time passcodes, mobile payload generation, and push notification approval workflows.
    • Added regional configuration options and structured MFA account, notification, OTP, and error information.
    • Added sample app screens for QR pairing, accounts, OTPs, payloads, and notification actions.
  • Documentation
    • Added setup instructions, API references, usage examples, and troubleshooting guidance.
  • Tests
    • Added comprehensive coverage for MFA workflows, errors, concurrency, notifications, and sample integrations.

gioraping and others added 23 commits May 25, 2026 17:55
… and PingExample sample app — including category registration, banner-action and foreground push routing, and an approve/deny modal with number-matching UI.

## What Was Built
Add PingOneMFA actionable push notification support to the SDK module and PingExample sample app — including category registration, banner-action and foreground push routing, and an approve/deny modal with number-matching UI.

## Key Decisions
- Category routing via runtime Set<UNNotificationCategory> (no hardcoded strings)
- Number selection IS approval for SELECT_NUMBER/ENTER_MANUALLY flows
- authenticationMethod: "user" hardcoded per InternalApp reference
- Modal-only — no new tab or navigation destination
- willPresent and didReceive retry initializePingOneMFAClient() when SDK is uninitialized

## Changes
- PingOneMFA/PingOneMFA/PingOneMFA.swift — getNotificationCategories(), processNotificationAction(), parseAPNSAlert helper
- PingOneMFA/PingOneMFA/PushNotification.swift — numberMatchingOptions, numberMatchingType, Identifiable conformance
- PingOneMFA/PingOneMFATests/ — mock tracking state + test21/22/23
- SampleApps/PingExample/PingExample/AppDelegate.swift — full PingOneMFA push wiring
- SampleApps/PingExample/PingExample/PingOneMFANotificationView.swift — new approve/deny modal
- SampleApps/PingExample/PingExample/ContentView.swift — ShowPingOneMFANotification sheet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…otificationView

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…class name collision

PingOneMFA.PushNotification is ambiguous at call sites that import PingOneMFA
because Swift resolves 'PingOneMFA' as the class, not the module. MFAPushNotification
is an unambiguous alias used in AppDelegate and ContentView.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…moteNotificationsWithDeviceToken

Matches the PingPush getInitializedPushClient() pattern: extract
ensurePingOneMFAInitialized() helper and call it in the token handler and
notification delegates instead of silently skipping when not yet initialized.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
passing geo diretcly to initialize
# Conflicts:
#	SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj
#	SampleApps/PingExample/PingExample/ContentView.swift
@gioraping
gioraping marked this pull request as ready for review June 1, 2026 15:19

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — it's a substantial, well-structured module and the doc comments across the public surface are genuinely excellent. 🙏 I've left inline comments below from a fairly pedantic pass focused on the new PingOneMFA SDK code (plus the README, tests, and the sample-app integration).

A few things I really liked: the Geo enum cleanly isolates PingOneGeo so consumers never import PingOneSDK, the same goes for not leaking NSError/PingOneSDKError, and getDeviceInfo()'s "neither data nor errors" branch correctly avoids hanging the continuation — exactly the defensive resume that prevents continuation-leak crashes. The AccountParserTests edge-case coverage is thorough too.

The handful of items I'd consider blocking before merge:

  • Racy idempotency in initialize(geo:) that contradicts its documented guarantee.
  • Background banner actions can be lost because completionHandler() fires before the async approve/deny completes.
  • README example for getDeviceInfo() doesn't compile (wrong return type).
  • Two tests that don't test what they claim — including a data race inside the "thread safety" test.

Everything else is lower-stakes polish. Really nice work overall — happy to talk through any of these. 🚀

Comment thread PingOneMFA/PingOneMFA/PingOneMFA.swift Outdated
Comment thread PingOneMFA/PingOneMFA/PingOneMFA.swift Outdated
Comment thread PingOneMFA/PingOneMFA/PingOneMFA.swift Outdated
Comment thread PingOneMFA/PingOneMFA/PingOneMFA.swift Outdated
Comment thread PingOneMFA/PingOneMFA/AccountParser.swift Outdated
Comment thread SampleApps/PingExample/PingExample/AppDelegate.swift Outdated
Comment thread SampleApps/PingExample/PingExample/AppDelegate.swift Outdated
Comment thread SampleApps/PingExample/PingExample/AppDelegate.swift Outdated
Comment thread SampleApps/PingExample/PingExample/PingOneMFAOtpViewModel.swift Outdated
Comment thread SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few more comments. Please confirm the lack of Podspec and maybe add a line in the Changelog

Comment thread PingOneMFA/PingOneMFA/PushType.swift
Comment thread PingOneMFA/README.md
Comment thread PingOneMFA/PingOneMFA/PingOneMFAError.swift Outdated
Comment thread PingOneMFA/PingOneMFA/PingOneMFAInternalError.swift Outdated
Comment thread SampleApps/PingExample/PingExample/AppDelegate.swift Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few small follow-ups from the latest push-flow changes — all minor/doc-level, nothing blocking. The redaction, Equatable, and the .dry sheet check all look good; thanks for those. 🙏 Left the specifics inline. (Also re: the podspec — saw the note that PingOneSDK is SPM-only, so disregard my earlier CocoaPods-parity comment; that's a reasonable constraint.)

Comment thread PingOneMFA/PingOneMFA/PingOneMfaAccount.swift Outdated
Comment thread PingOneMFA/PingOneMFA/PingOneMFA.swift Outdated
Comment thread PingOneMFA/README.md Outdated
Comment thread PingOneMFA/README.md Outdated
Comment thread SampleApps/PingExample/PingExample/AppDelegate.swift
Comment thread PingOneMFA/PingOneMFA/PushNotification.swift Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, please see the 1 new open comment. I would not merge this one yet, unless it has been cleared by QA

@rodrigoareis rodrigoareis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a new PingOneMFA Swift framework and package target backed by PingOneSDK, with async MFA APIs, public account/OTP/push models, error handling, tests, documentation, and PingExample app screens for pairing, accounts, OTP, payloads, and push actions.

Changes

PingOne MFA module

Layer / File(s) Summary
Package, framework, and test-project wiring
Package.swift, PingOneMFA/PingOneMFA.xcodeproj/..., PingTestHost/...
Adds the PingOneMFA product and target, pins pingone-mobile-sdk-ios to 2.3.1, configures framework/test targets, embeds PingOneSDK, includes privacy metadata, and registers the test scheme and plan.
Public contracts and SDK wrapper
PingOneMFA/PingOneMFA/*.swift
Adds geographic, account, OTP, error, push-type, and notification models, account parsing, actor-isolated initialization, async SDK operations, APNS handling, payload generation, and notification approval/denial APIs.
Tests and documentation
PingOneMFA/PingOneMFATests/*, PingOneMFA/README.md
Adds parser, error, initialization, concurrency, notification, model, and mock coverage, alongside setup, API, usage, troubleshooting, and notification-handling documentation.

PingExample integration

Layer / File(s) Summary
Framework and application routing
SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj, AppDelegate.swift, ConfigurationManager.swift, ContentView.swift
Links and embeds PingOneMFA, initializes it through ConfigurationManager, registers notification categories and device tokens, routes MFA notifications, and adds MFA navigation/menu entries.
MFA screens and view models
SampleApps/PingExample/PingExample/PingOneMFA*.swift
Adds QR/manual pairing, account listing, OTP countdown and refresh, mobile payload display/copy, and push approval/denial interfaces with async state and error handling.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant APNS
  participant AppDelegate
  participant PingOneMFA
  participant PingOneSDK
  participant ContentView
  APNS->>AppDelegate: deliver notification
  AppDelegate->>PingOneMFA: process notification or action
  PingOneMFA->>PingOneSDK: invoke notification API
  PingOneSDK-->>PingOneMFA: notification result
  PingOneMFA-->>AppDelegate: PushNotification?
  AppDelegate->>ContentView: post ShowPingOneMFANotification
  ContentView-->>PingOneMFA: approve or deny action
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too generic to clearly summarize the main change. Use a specific title like 'Add PingOneMFA module and sample app integration'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description includes a JIRA link, change summary, testing notes, and checklist, so it mostly matches the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (2)
PingOneMFA/PingOneMFATests/PingOneMFATests.swift (1)

482-493: 📐 Maintainability & Code Quality | 🔵 Trivial

Sleep-based polling instead of a proper async signal.

waitForConfigureCallCount polls via Task.sleep in a loop to detect when configure() has started. Since ConfigureProbe is already an actor, this could instead expose an async method that suspends until callCount reaches the threshold (e.g., its own continuation), avoiding wall-clock delays in the test.

As per path instructions, "Flag tests that use sleep or wall-clock delays instead of proper async/await or expectations."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingOneMFA/PingOneMFATests/PingOneMFATests.swift` around lines 482 - 493,
Replace the sleep-based polling in waitForConfigureCallCount with a proper async
signal owned by ConfigureProbe, such as a continuation-based method that
suspends until callCount reaches the requested minimum. Update the test helper
to await that method and preserve the existing threshold result without
wall-clock delays or retry limits.

Source: Path instructions

SampleApps/PingExample/PingExample/PingOneMFAAccountsView.swift (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a two-way Binding for error alerts instead of .constant(). Three views use .alert(..., isPresented: .constant(viewModel.errorMessage != nil)), while PingOneMFANotificationView.swift (same PR) correctly uses Binding(get:set:) for the equivalent alert. Functionally workable here, but inconsistent and slightly fragile if dismissal ever needs to originate from the system rather than the "OK" button.

  • SampleApps/PingExample/PingExample/PingOneMFAAccountsView.swift#L71-79: replace .constant(viewModel.errorMessage != nil) with a Binding(get: { viewModel.errorMessage != nil }, set: { if !$0 { viewModel.errorMessage = nil } }).
  • SampleApps/PingExample/PingExample/PingOneMFAOtpView.swift#L54-62: same replacement.
  • SampleApps/PingExample/PingExample/PingOneMFAPayloadView.swift#L44-52: same replacement.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SampleApps/PingExample/PingExample/PingOneMFAAccountsView.swift` at line 1,
Replace the constant alert bindings in PingOneMFAAccountsView,
PingOneMFAOtpView, and PingOneMFAPayloadView with two-way Binding(get:set:)
values that reflect whether errorMessage exists and clear viewModel.errorMessage
when the alert is dismissed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Package.swift`:
- Line 51: Add a root-level PingOneMFA.podspec corresponding to the PingOneMFA
library target declared in Package.swift. Match the repository’s existing
podspec conventions and configure the pod to expose the PingOneMFA target,
preserving distribution parity with SPM.
- Around line 308-317: Update the PingOneMFA target’s PingOneSDK product
dependency in the dependencies array to include a platform condition restricting
it to iOS via .when(platforms: [.iOS]); leave the target’s other configuration
unchanged.

In `@PingOneMFA/PingOneMFA/OtpCodeInfo.swift`:
- Around line 25-27: Update OtpCodeInfo.init(code:secondsRemaining:) to enforce
the documented non-negative validity invariant by clamping negative
secondsRemaining inputs to zero or explicitly rejecting them. Preserve the
provided value when it is zero or positive so consumers never receive an
impossible negative OTP state.

In `@PingOneMFA/PingOneMFA/PingOneMFAError.swift`:
- Around line 29-36: In PingOneMFAError, add /// documentation comments for the
public message property and errorDescription computed property, describing their
exposed error text. Leave internalErrorsList and the existing behavior
unchanged.

In `@PingOneMFA/PingOneMFATests/AccountParserTests.swift`:
- Around line 44-48: Replace direct accounts[0] and euAccounts[0] indexing in
the affected test assertions with try XCTUnwrap(accounts.first) and try
XCTUnwrap(euAccounts.first), reusing the unwrapped account values for subsequent
property checks. Preserve the existing count assertions and all expected field
values while ensuring failures produce normal XCTest failures instead of
out-of-bounds crashes.

In `@SampleApps/PingExample/PingExample/ContentView.swift`:
- Around line 248-249: Update the pingOneMFANotification state and its usage in
PingOneMFANotificationView to use the notification type exposed by the imported
PingOneMFA package, or adjust the view initializer to accept that package type;
ensure the related code at the notification handling block remains
type-compatible and the sample app compiles without relying on an undefined
local MFAPushNotification symbol.

In `@SampleApps/PingExample/PingExample/PingOneMFANotificationView.swift`:
- Around line 90-96: Update selectNumberSection so its number-matching buttons
are disabled or hidden whenever viewModel.isLoading is true, matching the
existing loading behavior in enterManuallySection. Ensure all number-selection
actions cannot trigger another approve() or approveNotification call while the
current request is in flight.

In `@SampleApps/PingExample/PingExample/PingOneMFAPayloadView.swift`:
- Around line 102-134: Update the copy action in copyButton to write the MFA
payload through UIPasteboard.setItems with pasteboard options that make it
local-only and set an expiration date, rather than assigning
UIPasteboard.general.string directly. Preserve the existing payload nil guard
and button behavior.

---

Nitpick comments:
In `@PingOneMFA/PingOneMFATests/PingOneMFATests.swift`:
- Around line 482-493: Replace the sleep-based polling in
waitForConfigureCallCount with a proper async signal owned by ConfigureProbe,
such as a continuation-based method that suspends until callCount reaches the
requested minimum. Update the test helper to await that method and preserve the
existing threshold result without wall-clock delays or retry limits.

In `@SampleApps/PingExample/PingExample/PingOneMFAAccountsView.swift`:
- Line 1: Replace the constant alert bindings in PingOneMFAAccountsView,
PingOneMFAOtpView, and PingOneMFAPayloadView with two-way Binding(get:set:)
values that reflect whether errorMessage exists and clear viewModel.errorMessage
when the alert is dismissed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 13817764-ef4a-4f25-bd97-e0fe6ee23cfc

📥 Commits

Reviewing files that changed from the base of the PR and between bb49373 and 6bb3170.

⛔ Files ignored due to path filters (2)
  • Package.resolved is excluded by !**/Package.resolved
  • SampleApps/Ping.xcworkspace/xcshareddata/swiftpm/Package.resolved is excluded by !**/Package.resolved
📒 Files selected for processing (35)
  • Package.swift
  • PingOneMFA/PingOneMFA.xcodeproj/project.pbxproj
  • PingOneMFA/PingOneMFA.xcodeproj/xcshareddata/IDETemplateMacros.plist
  • PingOneMFA/PingOneMFA.xcodeproj/xcshareddata/xcschemes/PingOneMFATests.xcscheme
  • PingOneMFA/PingOneMFA/AccountParser.swift
  • PingOneMFA/PingOneMFA/Geo.swift
  • PingOneMFA/PingOneMFA/OtpCodeInfo.swift
  • PingOneMFA/PingOneMFA/PingOneMFA.h
  • PingOneMFA/PingOneMFA/PingOneMFA.swift
  • PingOneMFA/PingOneMFA/PingOneMFAError.swift
  • PingOneMFA/PingOneMFA/PingOneMFAInternalError.swift
  • PingOneMFA/PingOneMFA/PingOneMfaAccount.swift
  • PingOneMFA/PingOneMFA/PrivacyInfo.xcprivacy
  • PingOneMFA/PingOneMFA/PushNotification.swift
  • PingOneMFA/PingOneMFA/PushType.swift
  • PingOneMFA/PingOneMFATests/AccountParserTests.swift
  • PingOneMFA/PingOneMFATests/MockPingOneMFA.swift
  • PingOneMFA/PingOneMFATests/PingOneMFAErrorTests.swift
  • PingOneMFA/PingOneMFATests/PingOneMFATests.swift
  • PingOneMFA/README.md
  • PingTestHost/PingTestHost.xcodeproj/xcshareddata/xcschemes/PingTestHost.xcscheme
  • PingTestHost/PingTestHost.xctestplan
  • SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj
  • SampleApps/PingExample/PingExample/AppDelegate.swift
  • SampleApps/PingExample/PingExample/ConfigurationManager.swift
  • SampleApps/PingExample/PingExample/ContentView.swift
  • SampleApps/PingExample/PingExample/PingOneMFAAccountsView.swift
  • SampleApps/PingExample/PingExample/PingOneMFAAccountsViewModel.swift
  • SampleApps/PingExample/PingExample/PingOneMFANotificationView.swift
  • SampleApps/PingExample/PingExample/PingOneMFAOtpView.swift
  • SampleApps/PingExample/PingExample/PingOneMFAOtpViewModel.swift
  • SampleApps/PingExample/PingExample/PingOneMFAPayloadView.swift
  • SampleApps/PingExample/PingExample/PingOneMFAPayloadViewModel.swift
  • SampleApps/PingExample/PingExample/PingOneMFAScannerContainerView.swift
  • SampleApps/PingExample/PingExample/PingOneMFAScannerViewModel.swift

Comment thread Package.swift
.library(name: "PingOath", targets: ["PingOath"]),
.library(name: "PingPush", targets: ["PingPush"]),
.library(name: "PingAuthMigration", targets: ["PingAuthMigration"]),
.library(name: "PingOneMFA", targets: ["PingOneMFA"]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'PingOneMFA.podspec'
rg -n "testTarget" Package.swift

Repository: ForgeRock/ping-ios-sdk

Length of output: 160


🏁 Script executed:

#!/bin/bash
set -u

echo "== Package.swift relevant sections =="
sed -n '40,70p' Package.swift
sed -n '290,325p' Package.swift

echo "== Files named PingOneMFA / PingOneMFA.* =="
git ls-files | rg -i '(^|/)PingOneMFA(\.podspec|\.xcodeproj|/.*)$|PingTestHost|Package\.manifest|Package\.swif'

echo "== All podspec-related files =="
git ls-files | rg '\.podspec$|Package\.swift'

echo "== testTarget occurrences =="
rg -n "\.testTarget|testTarget" -S Package.swift . 2>/dev/null || true

Repository: ForgeRock/ping-ios-sdk

Length of output: 5372


Add PingOneMFA.podspec for distribution parity.

Package.swift declares the new PingOneMFA library/target, and PingOneMFATests exists, but there is no corresponding root-level PingOneMFA.podspec. CocoaPods distribution of this SDK target will remain out of sync with SPM without it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Package.swift` at line 51, Add a root-level PingOneMFA.podspec corresponding
to the PingOneMFA library target declared in Package.swift. Match the
repository’s existing podspec conventions and configure the pod to expose the
PingOneMFA target, preserving distribution parity with SPM.

Source: Path instructions

Comment thread Package.swift
Comment on lines +308 to +317
.target(
name: "PingOneMFA",
dependencies: [
.product(name: "PingOneSDK", package: "pingone-mobile-sdk-ios")
],
path: "PingOneMFA/PingOneMFA",
exclude: ["PingOneMFA.h"],
resources: [.copy("PrivacyInfo.xcprivacy")]
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check top-level platforms declaration and whether other iOS-only vendor deps are platform-guarded
rg -n "platforms:" -A5 Package.swift
rg -n "\.when\(platforms:" Package.swift

Repository: ForgeRock/ping-ios-sdk

Length of output: 2100


🌐 Web query:

PingOne mobile SDK pingone-mobile-sdk-ios macOS support Package.swift

💡 Result:

The PingOne Mobile SDK for iOS does not currently provide official support for macOS. According to the official documentation and the repository's configuration, the SDK is explicitly designed for iOS and requires a minimum of iOS 15.0 and Xcode 14 or later [1][2][3]. The project's Package.swift file defines the SDK as a library intended for iOS integration [4]. While the SDK can be installed via the Swift Package Manager, the compatibility guidelines provided by Ping Identity consistently specify only iOS as the target platform [1][3]. There is no mention of macOS, iPadOS, or other Apple platforms in the official support requirements or the package definition [1][4][3].

Citations:


Guard PingOneSDK to iOS for the PingOneMFA dependency.

Package.swift declares macOS 13 support, but pingone-mobile-sdk-ios is iOS-only with no macOS support. Add .when(platforms: [.iOS]) to the PingOneSDK product dependency here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Package.swift` around lines 308 - 317, Update the PingOneMFA target’s
PingOneSDK product dependency in the dependencies array to include a platform
condition restricting it to iOS via .when(platforms: [.iOS]); leave the target’s
other configuration unchanged.

Source: Coding guidelines

Comment on lines +25 to +27
public init(code: String, secondsRemaining: Int) {
self.code = code
self.secondsRemaining = secondsRemaining

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Enforce the non-negative OTP validity invariant.

The public initializer accepts negative secondsRemaining values even though the API documents expired codes as 0. Clamp negative inputs to zero or reject them explicitly; otherwise consumers can receive an impossible OTP state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingOneMFA/PingOneMFA/OtpCodeInfo.swift` around lines 25 - 27, Update
OtpCodeInfo.init(code:secondsRemaining:) to enforce the documented non-negative
validity invariant by clamping negative secondsRemaining inputs to zero or
explicitly rejecting them. Preserve the provided value when it is zero or
positive so consumers never receive an impossible negative OTP state.

Comment on lines +29 to +36
public struct PingOneMFAError: Error, LocalizedError, Sendable {
public let message: String

/// Structured list of individual SDK errors, or `nil` when the failure did not originate
/// from the native SDK (e.g. an unexpected exception).
public let internalErrorsList: [PingOneMFAInternalError]?

public var errorDescription: String? { message }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Document all public error members.

Add /// documentation for message and errorDescription; both are part of the public SDK surface and currently lack member-level documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingOneMFA/PingOneMFA/PingOneMFAError.swift` around lines 29 - 36, In
PingOneMFAError, add /// documentation comments for the public message property
and errorDescription computed property, describing their exposed error text.
Leave internalErrorsList and the existing behavior unchanged.

Source: Path instructions

Comment on lines +44 to +48
XCTAssertEqual(accounts.count, 1)
XCTAssertEqual(accounts[0].region, "NorthAmerica")
XCTAssertEqual(accounts[0].id, "c845dcd4-9696-45ce-b1b8-8797da941538")
XCTAssertEqual(accounts[0].deviceId, "05280532-42b0-4d29-93f2-9f2ed7acefc1")
XCTAssertEqual(accounts[0].environmentId, "803ca4d4-cd92-4cb8-9dd1-6fe68de0a5f0")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid unchecked array indexing after assertions.

If a preceding count assertion fails, accounts[0] or euAccounts[0] can crash the test. Use try XCTUnwrap(accounts.first) / try XCTUnwrap(euAccounts.first) so the test reports a normal XCTest failure.

Also applies to: 83-90, 127-129

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingOneMFA/PingOneMFATests/AccountParserTests.swift` around lines 44 - 48,
Replace direct accounts[0] and euAccounts[0] indexing in the affected test
assertions with try XCTUnwrap(accounts.first) and try
XCTUnwrap(euAccounts.first), reusing the unwrapped account values for subsequent
property checks. Preserve the existing count assertions and all expected field
values while ensuring failures produce normal XCTest failures instead of
out-of-bounds crashes.

Source: Path instructions

Comment on lines +248 to +249
@State private var pingOneMFANotification: MFAPushNotification? = nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether MFAPushNotification is a typealias/alias of PushNotification.
rg -n 'MFAPushNotification' --type=swift -C2
rg -n 'typealias\s+MFAPushNotification|struct\s+PushNotification|class\s+PushNotification|typealias\s+PushNotification' --type=swift

Repository: ForgeRock/ping-ios-sdk

Length of output: 295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Swift files containing PingOneMFANotificationView =="
rg -n 'PingOneMFANotificationView|processRemoteNotification|PushNotification|MFAPushNotification|import PingOneMFA' --type=swift -C 3

echo
echo "== Locate candidate files =="
fd -e swift -i 'PingOneMFANotificationView|SampleApps/PingExample/PingExample/ContentView.swift|AppDelegate.swift' .

Repository: ForgeRock/ping-ios-sdk

Length of output: 383


Ensure MFA notification type compatibility.

pingOneMFANotification is received as MFAPushNotification and passed to PingOneMFANotificationView, but this module does not define MFAPushNotification or alias it to PushNotification. Gate this behind the PingOneMFA package API (or adjust the view initializer) so the sample app compiles against the dependency it imports. Also applies to lines 281-288.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SampleApps/PingExample/PingExample/ContentView.swift` around lines 248 - 249,
Update the pingOneMFANotification state and its usage in
PingOneMFANotificationView to use the notification type exposed by the imported
PingOneMFA package, or adjust the view initializer to accept that package type;
ensure the related code at the notification handling block remains
type-compatible and the sample app compiles without relying on an undefined
local MFAPushNotification symbol.

Comment on lines +90 to +96
if viewModel.notification.pushType == .challenge {
if !viewModel.notification.getNumbersChallenge.isEmpty {
selectNumberSection
} else {
enterManuallySection
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Number-matching buttons stay tappable during isLoading, unlike the manual-entry path.

enterManuallySection's confirm button disappears while viewModel.isLoading (line 231), but selectNumberSection's number buttons have no such guard — a user can tap another number (or the same one twice) while an approve() call is in flight, firing concurrent approveNotification calls against the SDK.

🔒 Suggested fix
             let options = viewModel.notification.getNumbersChallenge
             if options.isEmpty {
                 Text("No options available")
                     .font(.system(size: 14))
                     .foregroundColor(.red)
             } else {
                 HStack(spacing: 16) {
                     ForEach(options, id: \.self) { number in
                         Button {
                             viewModel.approve(numberChallenge: number)
                         } label: {
                             Text("\(number)")
                                 .font(.system(size: 24, weight: .bold))
                                 .foregroundColor(.themeButtonBackground)
                                 .frame(width: 80, height: 80)
                                 .background(Color.clear)
                                 .overlay(
                                     Circle()
                                         .stroke(Color.themeButtonBackground, lineWidth: 2)
                                 )
                         }
+                        .disabled(viewModel.isLoading)
                     }
                 }
             }

Also applies to: 182-214

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SampleApps/PingExample/PingExample/PingOneMFANotificationView.swift` around
lines 90 - 96, Update selectNumberSection so its number-matching buttons are
disabled or hidden whenever viewModel.isLoading is true, matching the existing
loading behavior in enterManuallySection. Ensure all number-selection actions
cannot trigger another approve() or approveNotification call while the current
request is in flight.

Comment on lines +102 to +134
private var copyButton: some View {
Button {
if let payload = viewModel.payload {
UIPasteboard.general.string = payload
}
} label: {
HStack(spacing: 8) {
Image(systemName: "doc.on.doc")
.font(.system(size: 16))
Text("Copy")
.font(.system(size: 16, weight: .medium))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(
viewModel.payload != nil
? LinearGradient(
colors: [.themeButtonBackground, Color(red: 0.6, green: 0.1, blue: 0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
: LinearGradient(
colors: [Color.gray, Color.gray],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.disabled(viewModel.payload == nil)
.accessibilityIdentifier("copyPayloadButton")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Copying MFA payload to the general pasteboard.

copyButton writes the mobile payload — generated for authentication/registration — to UIPasteboard.general, which persists and is readable by other apps. Consider setItems with .expirationDate/.localOnly to limit exposure of what may be sensitive authentication material.

🛡️ Suggested fix
             if let payload = viewModel.payload {
-                UIPasteboard.general.string = payload
+                UIPasteboard.general.setItems(
+                    [[UIPasteboard.typeAutomatic: payload]],
+                    options: [.expirationDate: Date().addingTimeInterval(60), .localOnly: true]
+                )
             }

As per path instructions, sample app code should still flag security issues involving real tokens.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private var copyButton: some View {
Button {
if let payload = viewModel.payload {
UIPasteboard.general.string = payload
}
} label: {
HStack(spacing: 8) {
Image(systemName: "doc.on.doc")
.font(.system(size: 16))
Text("Copy")
.font(.system(size: 16, weight: .medium))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(
viewModel.payload != nil
? LinearGradient(
colors: [.themeButtonBackground, Color(red: 0.6, green: 0.1, blue: 0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
: LinearGradient(
colors: [Color.gray, Color.gray],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.disabled(viewModel.payload == nil)
.accessibilityIdentifier("copyPayloadButton")
}
private var copyButton: some View {
Button {
if let payload = viewModel.payload {
UIPasteboard.general.setItems(
[[UIPasteboard.typeAutomatic: payload]],
options: [.expirationDate: Date().addingTimeInterval(60), .localOnly: true]
)
}
} label: {
HStack(spacing: 8) {
Image(systemName: "doc.on.doc")
.font(.system(size: 16))
Text("Copy")
.font(.system(size: 16, weight: .medium))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(
viewModel.payload != nil
? LinearGradient(
colors: [.themeButtonBackground, Color(red: 0.6, green: 0.1, blue: 0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
: LinearGradient(
colors: [Color.gray, Color.gray],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.disabled(viewModel.payload == nil)
.accessibilityIdentifier("copyPayloadButton")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SampleApps/PingExample/PingExample/PingOneMFAPayloadView.swift` around lines
102 - 134, Update the copy action in copyButton to write the MFA payload through
UIPasteboard.setItems with pasteboard options that make it local-only and set an
expiration date, rather than assigning UIPasteboard.general.string directly.
Preserve the existing payload nil guard and button behavior.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants