diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/.github/copilot-instructions.md b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/.github/copilot-instructions.md new file mode 100644 index 00000000..8e82ed24 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/.github/copilot-instructions.md @@ -0,0 +1,14 @@ +# Copilot Custom Instructions + +- This repository uses Swift 6.1+ and SwiftUI for iOS 18+ apps. All code should follow modern Swift and SwiftUI best practices. +- This is an iOS project NOT a pure Swift Package or macOS project. It utlises a local Swift Package which is wrapped in an Xcode project. This makes it easier for agents to work on the project. +- Use the Model-View (MV) pattern with native SwiftUI state management (`@State`, `@Observable`, `@Environment`, `@Binding`). Do not use ViewModels or MVVM. +- All concurrency must use Swift Concurrency (async/await, actors, @MainActor). Do not use GCD or completion handlers. +- Write all new code and features inside the Swift Package (`YourAppPackage`), not in the app shell. +- Use the Swift Testing framework (`@Test`, `#expect`, `#require`) for all tests. Place tests in the package's `Tests/` directory. +- When running tests use the `test_sim_name_ws` tool do not use `swift_package_test`.- +- Use XcodeBuildMCP tools for building, testing, and automation. Prefer these over raw xcodebuild or CLI commands. +- For data persistence, use SwiftData (never CoreData), though only use for complex scenarios, prefer simpler options first e.g. UserDefaults. +- Always provide accessibility labels and identifiers for UI elements. +- Never log sensitive information or use insecure network calls. +- For full style, architecture, and workflow details, refer to the project documentation in [`template/.cursor/rules/`](../.cursor/rules/). diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/.gitignore b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/.gitignore new file mode 100644 index 00000000..fda4de3a --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/.gitignore @@ -0,0 +1,62 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output \ No newline at end of file diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Debug.xcconfig b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Debug.xcconfig new file mode 100644 index 00000000..75b2eb20 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Debug.xcconfig @@ -0,0 +1,8 @@ +// Debug.xcconfig +// Debug configuration for iOS projects - minimal overrides only +// Generated by XcodeBuildMCP + +#include "Shared.xcconfig" + +// No additional debug-specific overrides needed +// All debug settings use Xcode project defaults \ No newline at end of file diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/PasskeysSwiftUI.entitlements b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/PasskeysSwiftUI.entitlements new file mode 100644 index 00000000..06ee6669 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/PasskeysSwiftUI.entitlements @@ -0,0 +1,11 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:your-server-domain.com?mode=develop + webcredentials:your-server-domain.com + + + diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Release.xcconfig b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Release.xcconfig new file mode 100644 index 00000000..67bf1f04 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Release.xcconfig @@ -0,0 +1,8 @@ +// Release.xcconfig +// Release configuration for iOS projects - minimal overrides only +// Generated by XcodeBuildMCP + +#include "Shared.xcconfig" + +// No additional release-specific overrides needed +// All release settings use Xcode project defaults \ No newline at end of file diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Shared.xcconfig b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Shared.xcconfig new file mode 100644 index 00000000..e41ee57a --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Shared.xcconfig @@ -0,0 +1,39 @@ +// Shared.xcconfig +// Minimal shared configuration for scaffold tool customization +// All other settings use Xcode project defaults +// Generated by XcodeBuildMCP + +// ========================================== +// Project Identity +// ========================================== +PRODUCT_NAME = PasskeysSwiftUI +PRODUCT_DISPLAY_NAME = PasskeysSwiftUI +PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.passkeysswiftui +MARKETING_VERSION = 1.0.0 +CURRENT_PROJECT_VERSION = 1 + +// ========================================== +// Platform Configuration +// ========================================== +IPHONEOS_DEPLOYMENT_TARGET = 18.0 + +// (1 == iPhone, 2 == iPad) +TARGETED_DEVICE_FAMILY = 1,2 + +// ========================================== +// Info PLIST +// ========================================== +GENERATE_INFOPLIST_FILE = YES +INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationLandscapeRight UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationPortrait +INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationLandscapeRight UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown + +// ========================================== +// Info PLIST Keys +// ========================================== +INFOPLIST_KEY_NSFaceIDUsageDescription = This app uses Face ID for biometric authentication with passkeys. + +// ========================================== +// Entitlements +// ========================================== +// AI agents can modify Config/PasskeysSwiftUI.entitlements to add capabilities +CODE_SIGN_ENTITLEMENTS = Config/PasskeysSwiftUI.entitlements diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Tests.xcconfig b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Tests.xcconfig new file mode 100644 index 00000000..49318f6b --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/Config/Tests.xcconfig @@ -0,0 +1,14 @@ +// Tests.xcconfig +// Test configuration for iOS projects - minimal overrides only +// Generated by XcodeBuildMCP + +#include "Shared.xcconfig" + +// ========================================== +// Test Target Settings (Customizable by scaffold tool) +// ========================================== +PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.passkeysswiftui +TEST_TARGET_NAME = PasskeysSwiftUI + +// Fix duplicate module name issue +PRODUCT_MODULE_NAME = $(PRODUCT_NAME)UITests \ No newline at end of file diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcodeproj/project.pbxproj b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcodeproj/project.pbxproj new file mode 100644 index 00000000..8f15acbd --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcodeproj/project.pbxproj @@ -0,0 +1,538 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 43D0BE5DEADE4B9F1BB3EB28 /* PingJourney in Frameworks */ = {isa = PBXBuildFile; productRef = 81158D7A074F1B72DD4C07DE /* PingJourney */; }; + 651E879EEB931228CEF2D31A /* PingOidc in Frameworks */ = {isa = PBXBuildFile; productRef = 06B04414648276D65C9BDF68 /* PingOidc */; }; + 93D1A04D884C046026324BFC /* PingOrchestrate in Frameworks */ = {isa = PBXBuildFile; productRef = 4A04B7FD6BA345628F49F01E /* PingOrchestrate */; }; + 9CF36A7813727C9D8D162716 /* PingJourneyPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 62EB33A1CFDCF3A274C93942 /* PingJourneyPlugin */; }; + EC9C5CCB67FD2FC3CA415125 /* PingLogger in Frameworks */ = {isa = PBXBuildFile; productRef = E256473691CDBA1288A61EF4 /* PingLogger */; }; + EE1BA5E17E12F97795B4A26B /* PingFido in Frameworks */ = {isa = PBXBuildFile; productRef = 9076D702DE7ED6A5139E4106 /* PingFido */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 8B41F65D2DEDD0D6001A66F9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8B41F63D2DEDD0D5001A66F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8B41F6442DEDD0D5001A66F9; + remoteInfo = PasskeysSwiftUI; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 8B41F6452DEDD0D5001A66F9 /* PasskeysSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PasskeysSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8B41F65C2DEDD0D6001A66F9 /* PasskeysSwiftUI.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PasskeysSwiftUI.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 8BD71C0A2DEE41E000CEDD92 /* Exceptions for "Config" folder in "PasskeysSwiftUI" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Debug.xcconfig, + Release.xcconfig, + Shared.xcconfig, + Tests.xcconfig, + ); + target = 8B41F6442DEDD0D5001A66F9 /* PasskeysSwiftUI */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 8B41F6472DEDD0D5001A66F9 /* PasskeysSwiftUI */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = PasskeysSwiftUI; + sourceTree = ""; + }; + 8B41F65F2DEDD0D6001A66F9 /* PasskeysSwiftUIUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = PasskeysSwiftUIUITests; + sourceTree = ""; + }; + 8BD71C052DEE41D800CEDD92 /* Config */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 8BD71C0A2DEE41E000CEDD92 /* Exceptions for "Config" folder in "PasskeysSwiftUI" target */, + ); + path = Config; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8B41F6422DEDD0D5001A66F9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 43D0BE5DEADE4B9F1BB3EB28 /* PingJourney in Frameworks */, + EE1BA5E17E12F97795B4A26B /* PingFido in Frameworks */, + 651E879EEB931228CEF2D31A /* PingOidc in Frameworks */, + 93D1A04D884C046026324BFC /* PingOrchestrate in Frameworks */, + EC9C5CCB67FD2FC3CA415125 /* PingLogger in Frameworks */, + 9CF36A7813727C9D8D162716 /* PingJourneyPlugin in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8B41F6592DEDD0D6001A66F9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8B41F63C2DEDD0D5001A66F9 = { + isa = PBXGroup; + children = ( + 8BD71C052DEE41D800CEDD92 /* Config */, + 8B41F6472DEDD0D5001A66F9 /* PasskeysSwiftUI */, + 8B41F65F2DEDD0D6001A66F9 /* PasskeysSwiftUIUITests */, + 8B41F6812DEDD23B001A66F9 /* Frameworks */, + 8B41F6462DEDD0D5001A66F9 /* Products */, + ); + sourceTree = ""; + }; + 8B41F6462DEDD0D5001A66F9 /* Products */ = { + isa = PBXGroup; + children = ( + 8B41F6452DEDD0D5001A66F9 /* PasskeysSwiftUI.app */, + 8B41F65C2DEDD0D6001A66F9 /* PasskeysSwiftUI.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 8B41F6812DEDD23B001A66F9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8B41F6442DEDD0D5001A66F9 /* PasskeysSwiftUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8B41F6662DEDD0D6001A66F9 /* Build configuration list for PBXNativeTarget "PasskeysSwiftUI" */; + buildPhases = ( + 8B41F6412DEDD0D5001A66F9 /* Sources */, + 8B41F6422DEDD0D5001A66F9 /* Frameworks */, + 8B41F6432DEDD0D5001A66F9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 8B41F6472DEDD0D5001A66F9 /* PasskeysSwiftUI */, + 8BD71C052DEE41D800CEDD92 /* Config */, + ); + name = PasskeysSwiftUI; + packageProductDependencies = ( + 81158D7A074F1B72DD4C07DE /* PingJourney */, + 9076D702DE7ED6A5139E4106 /* PingFido */, + 06B04414648276D65C9BDF68 /* PingOidc */, + 4A04B7FD6BA345628F49F01E /* PingOrchestrate */, + E256473691CDBA1288A61EF4 /* PingLogger */, + 62EB33A1CFDCF3A274C93942 /* PingJourneyPlugin */, + ); + productName = PasskeysSwiftUI; + productReference = 8B41F6452DEDD0D5001A66F9 /* PasskeysSwiftUI.app */; + productType = "com.apple.product-type.application"; + }; + 8B41F65B2DEDD0D6001A66F9 /* PasskeysSwiftUIUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8B41F66C2DEDD0D6001A66F9 /* Build configuration list for PBXNativeTarget "PasskeysSwiftUIUITests" */; + buildPhases = ( + 8B41F6582DEDD0D6001A66F9 /* Sources */, + 8B41F6592DEDD0D6001A66F9 /* Frameworks */, + 8B41F65A2DEDD0D6001A66F9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 8B41F65E2DEDD0D6001A66F9 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 8B41F65F2DEDD0D6001A66F9 /* PasskeysSwiftUIUITests */, + ); + name = PasskeysSwiftUIUITests; + productName = PasskeysSwiftUIUITests; + productReference = 8B41F65C2DEDD0D6001A66F9 /* PasskeysSwiftUI.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8B41F63D2DEDD0D5001A66F9 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + TargetAttributes = { + 8B41F6442DEDD0D5001A66F9 = { + CreatedOnToolsVersion = 16.3; + }; + 8B41F65B2DEDD0D6001A66F9 = { + CreatedOnToolsVersion = 16.3; + TestTargetID = 8B41F6442DEDD0D5001A66F9; + }; + }; + }; + buildConfigurationList = 8B41F6402DEDD0D5001A66F9 /* Build configuration list for PBXProject "PasskeysSwiftUI" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8B41F63C2DEDD0D5001A66F9; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 2E4B7E570803C45A04F1658C /* XCRemoteSwiftPackageReference "ping-ios-sdk" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 8B41F6462DEDD0D5001A66F9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8B41F6442DEDD0D5001A66F9 /* PasskeysSwiftUI */, + 8B41F65B2DEDD0D6001A66F9 /* PasskeysSwiftUIUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8B41F6432DEDD0D5001A66F9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8B41F65A2DEDD0D6001A66F9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8B41F6412DEDD0D5001A66F9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8B41F6582DEDD0D6001A66F9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 8B41F65E2DEDD0D6001A66F9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8B41F6442DEDD0D5001A66F9 /* PasskeysSwiftUI */; + targetProxy = 8B41F65D2DEDD0D6001A66F9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 8B41F6642DEDD0D6001A66F9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 8B41F6652DEDD0D6001A66F9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8B41F6672DEDD0D6001A66F9 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 8BD71C052DEE41D800CEDD92 /* Config */; + baseConfigurationReferenceRelativePath = Debug.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 9QSE66762D; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = PasskeysSwiftUI; + INFOPLIST_KEY_NSFaceIDUsageDescription = "This app uses Face ID for biometric authentication with passkeys."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeRight UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeRight UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.forgerock.unsummit; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 8B41F6682DEDD0D6001A66F9 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 8BD71C052DEE41D800CEDD92 /* Config */; + baseConfigurationReferenceRelativePath = Release.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 9QSE66762D; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = PasskeysSwiftUI; + INFOPLIST_KEY_NSFaceIDUsageDescription = "This app uses Face ID for biometric authentication with passkeys."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeRight UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeRight UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.forgerock.unsummit; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 8B41F66D2DEDD0D6001A66F9 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 8BD71C052DEE41D800CEDD92 /* Config */; + baseConfigurationReferenceRelativePath = Tests.xcconfig; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + GENERATE_INFOPLIST_FILE = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = PasskeysSwiftUI; + }; + name = Debug; + }; + 8B41F66E2DEDD0D6001A66F9 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 8BD71C052DEE41D800CEDD92 /* Config */; + baseConfigurationReferenceRelativePath = Tests.xcconfig; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + GENERATE_INFOPLIST_FILE = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = PasskeysSwiftUI; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8B41F6402DEDD0D5001A66F9 /* Build configuration list for PBXProject "PasskeysSwiftUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B41F6642DEDD0D6001A66F9 /* Debug */, + 8B41F6652DEDD0D6001A66F9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8B41F6662DEDD0D6001A66F9 /* Build configuration list for PBXNativeTarget "PasskeysSwiftUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B41F6672DEDD0D6001A66F9 /* Debug */, + 8B41F6682DEDD0D6001A66F9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8B41F66C2DEDD0D6001A66F9 /* Build configuration list for PBXNativeTarget "PasskeysSwiftUIUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B41F66D2DEDD0D6001A66F9 /* Debug */, + 8B41F66E2DEDD0D6001A66F9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2E4B7E570803C45A04F1658C /* XCRemoteSwiftPackageReference "ping-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ForgeRock/ping-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 06B04414648276D65C9BDF68 /* PingOidc */ = { + isa = XCSwiftPackageProductDependency; + package = 2E4B7E570803C45A04F1658C /* XCRemoteSwiftPackageReference "ping-ios-sdk" */; + productName = PingOidc; + }; + 4A04B7FD6BA345628F49F01E /* PingOrchestrate */ = { + isa = XCSwiftPackageProductDependency; + package = 2E4B7E570803C45A04F1658C /* XCRemoteSwiftPackageReference "ping-ios-sdk" */; + productName = PingOrchestrate; + }; + 62EB33A1CFDCF3A274C93942 /* PingJourneyPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 2E4B7E570803C45A04F1658C /* XCRemoteSwiftPackageReference "ping-ios-sdk" */; + productName = PingJourneyPlugin; + }; + 81158D7A074F1B72DD4C07DE /* PingJourney */ = { + isa = XCSwiftPackageProductDependency; + package = 2E4B7E570803C45A04F1658C /* XCRemoteSwiftPackageReference "ping-ios-sdk" */; + productName = PingJourney; + }; + 9076D702DE7ED6A5139E4106 /* PingFido */ = { + isa = XCSwiftPackageProductDependency; + package = 2E4B7E570803C45A04F1658C /* XCRemoteSwiftPackageReference "ping-ios-sdk" */; + productName = PingFido; + }; + E256473691CDBA1288A61EF4 /* PingLogger */ = { + isa = XCSwiftPackageProductDependency; + package = 2E4B7E570803C45A04F1658C /* XCRemoteSwiftPackageReference "ping-ios-sdk" */; + productName = PingLogger; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 8B41F63D2DEDD0D5001A66F9 /* Project object */; +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..c1f9eb91 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,114 @@ +{ + "originHash" : "956d622730eea977a9714ff7bca8e26297309c8b894a7bbf03c3f9f9b8d6d312", + "pins" : [ + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "145104f5ea9d58ae21b60add007c33c1cc0c948e", + "version" : "2.0.0" + } + }, + { + "identity" : "facebook-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/facebook/facebook-ios-sdk.git", + "state" : { + "revision" : "3fe31c168903759de1c5752d12856c5c437c6862", + "version" : "16.3.1" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS.git", + "state" : { + "revision" : "3996d908c7b3ce8a87d39c808f9a6b2a08fbe043", + "version" : "9.0.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "56e0ccf09a6dd29dc7e68bdf729598240ca8aa16", + "version" : "5.0.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "ping-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ForgeRock/ping-ios-sdk", + "state" : { + "revision" : "d025874d598d7337a72f0a8ee446dcddae7b6ef2", + "version" : "2.0.0" + } + }, + { + "identity" : "pingone-signals-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pingidentity/pingone-signals-sdk-ios.git", + "state" : { + "revision" : "e41f7070fdbb43dd7762274ec610c099256a7c7e", + "version" : "5.4.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "recaptcha-enterprise-mobile-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk.git", + "state" : { + "revision" : "fb634e89a36fd91725ad654d5576d800c061b37d", + "version" : "18.8.2" + } + } + ], + "version" : 3 +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcodeproj/xcshareddata/xcschemes/PasskeysSwiftUI.xcscheme b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcodeproj/xcshareddata/xcschemes/PasskeysSwiftUI.xcscheme new file mode 100644 index 00000000..6190055b --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcodeproj/xcshareddata/xcschemes/PasskeysSwiftUI.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/App/PasskeysApp.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/App/PasskeysApp.swift new file mode 100644 index 00000000..ae9e3f43 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/App/PasskeysApp.swift @@ -0,0 +1,20 @@ +// +// PasskeysApp.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI + +@main +struct PasskeysApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/Contents.json b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/Logo.imageset/Contents.json b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/Logo.imageset/Contents.json new file mode 100644 index 00000000..1acb1006 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/Logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Ping Identity Logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/Logo.imageset/Ping Identity Logo.png b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/Logo.imageset/Ping Identity Logo.png new file mode 100644 index 00000000..2218aa29 Binary files /dev/null and b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Assets.xcassets/Logo.imageset/Ping Identity Logo.png differ diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Config/AppConfiguration.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Config/AppConfiguration.swift new file mode 100644 index 00000000..ef55f668 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Config/AppConfiguration.swift @@ -0,0 +1,57 @@ +// +// AppConfiguration.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import PingJourney +import PingStorage +import PingLogger + +enum JourneyName { + static let login = "Login" + static let fidoRegistration = "BlogWebAuthnRegistration" + static let fidoAuthentication = "BlogWebAuthnAuthentication" +} + +enum UserDefaultsKey { + static let biometricsEnabled = "BiometricsEnabled" +} + +enum ServerConfig { + static let serverUrl = "<#YOUR_SERVER_URL#>" // e.g. "https://your-tenant.forgeblocks.com/am" + static let realm = "<#YOUR_REALM#>" // e.g. "alpha" + static let cookieName = "<#YOUR_COOKIE_NAME#>" // Session cookie name from your AM config + static let clientId = "<#YOUR_CLIENT_ID#>" // OAuth 2.0 client ID + static let scopes: Set = ["openid", "profile", "email", "address"] + static let redirectUri = "<#YOUR_REDIRECT_URI#>" // Must match your OAuth 2.0 client config + static let discoveryEndpoint = "<#YOUR_DISCOVERY_ENDPOINT#>" // e.g. "https://your-tenant.forgeblocks.com/am/oauth2//.well-known/openid-configuration" +} + +@MainActor +class AppJourney { + static let shared = AppJourney() + + let journey: Journey + + private init() { + journey = Journey.createJourney { journeyConfig in + journeyConfig.serverUrl = ServerConfig.serverUrl + journeyConfig.realm = ServerConfig.realm + journeyConfig.cookie = ServerConfig.cookieName + journeyConfig.logger = LogManager.standard + journeyConfig.module(PingJourney.OidcModule.config) { oidcConfig in + oidcConfig.clientId = ServerConfig.clientId + oidcConfig.scopes = ServerConfig.scopes + oidcConfig.redirectUri = ServerConfig.redirectUri + oidcConfig.discoveryEndpoint = ServerConfig.discoveryEndpoint + oidcConfig.logger = LogManager.standard + } + } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/PasskeysSwiftUI.xctestplan b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/PasskeysSwiftUI.xctestplan new file mode 100644 index 00000000..b6272f7a --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/PasskeysSwiftUI.xctestplan @@ -0,0 +1,35 @@ +{ + "configurations" : [ + { + "id" : "24499A57-8A8C-49DD-9DF6-FD06943246D4", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:PasskeysSwiftUI.xcodeproj", + "identifier" : "8B41F6442DEDD0D5001A66F9", + "name" : "PasskeysSwiftUI" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:PasskeysSwiftUIPackage", + "identifier" : "PasskeysSwiftUIFeatureTests", + "name" : "PasskeysSwiftUIFeatureTests" + } + }, + { + "target" : { + "containerPath" : "container:PasskeysSwiftUI.xcodeproj", + "identifier" : "8B41F65B2DEDD0D6001A66F9", + "name" : "PasskeysSwiftUIUITests" + } + } + ], + "version" : 1 +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/ViewModels/AuthenticatedViewModel.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/ViewModels/AuthenticatedViewModel.swift new file mode 100644 index 00000000..230e2556 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/ViewModels/AuthenticatedViewModel.swift @@ -0,0 +1,45 @@ +// +// AuthenticatedViewModel.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Observation +import PingJourney +import PingOidc + +@Observable +@MainActor +final class AuthenticatedViewModel { + var userInfo: [String: Any] = [:] + var isLoadingUserInfo = false + var userInfoError: String? + var isSignedOut = false + + func loadUserInfo() async { + isLoadingUserInfo = true + userInfoError = nil + defer { isLoadingUserInfo = false } + + guard let user = await AppJourney.shared.journey.journeyUser() else { + userInfoError = "No authenticated user found." + return + } + + switch await user.userinfo(cache: false) { + case .success(let info): + userInfo = info + case .failure(let error): + userInfoError = error.localizedDescription + } + } + + func signOut() async { + _ = await AppJourney.shared.journey.signOff() + isSignedOut = true + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/ViewModels/LoginViewModel.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/ViewModels/LoginViewModel.swift new file mode 100644 index 00000000..7eccdb83 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/ViewModels/LoginViewModel.swift @@ -0,0 +1,44 @@ +// +// LoginViewModel.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import Observation +import PingJourney +import PingOrchestrate + +@Observable +@MainActor +final class LoginViewModel { + var node: Node? + var isLoading = false + + func start() async { + let journeyName = UserDefaults.standard.bool(forKey: UserDefaultsKey.biometricsEnabled) + ? JourneyName.fidoAuthentication + : JourneyName.login + await startJourney(name: journeyName) + } + + func startJourney(name: String) async { + isLoading = true + defer { isLoading = false } + node = await AppJourney.shared.journey.start(name) + } + + func next(continueNode: ContinueNode) async { + isLoading = true + defer { isLoading = false } + node = await continueNode.next() + } + + func reset() { + node = nil + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/ViewModels/RegistrationViewModel.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/ViewModels/RegistrationViewModel.swift new file mode 100644 index 00000000..317290b7 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/ViewModels/RegistrationViewModel.swift @@ -0,0 +1,46 @@ +// +// RegistrationViewModel.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import Observation +import PingJourney +import PingOrchestrate + +@Observable +@MainActor +final class RegistrationViewModel { + var node: Node? + var isLoading = false + var isRegistered = false + var isFailure = false + + func startRegistration() async { + isLoading = true + isRegistered = false + isFailure = false + defer { isLoading = false } + node = await AppJourney.shared.journey.start(JourneyName.fidoRegistration) { options in + options.forceAuth = true + } + } + + func next(continueNode: ContinueNode) async { + isLoading = true + defer { isLoading = false } + let nextNode = await continueNode.next() + node = nextNode + if nextNode is SuccessNode { + UserDefaults.standard.set(true, forKey: UserDefaultsKey.biometricsEnabled) + isRegistered = true + } else if nextNode is FailureNode || nextNode is ErrorNode { + isFailure = true + } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/AuthenticatedView.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/AuthenticatedView.swift new file mode 100644 index 00000000..6351c3f1 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/AuthenticatedView.swift @@ -0,0 +1,182 @@ +// +// AuthenticatedView.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingJourney +import PingOrchestrate + +struct AuthenticatedView: View { + let successNode: SuccessNode + let onSignOut: () -> Void + + @State private var viewModel = AuthenticatedViewModel() + @State private var showSettings = false + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 0) { + AuthenticatedHeaderView() + + VStack(spacing: 20) { + profileSection + signOutButton + } + .padding(24) + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { settingsToolbarItem } + .sheet(isPresented: $showSettings) { SettingsView() } + .onChange(of: viewModel.isSignedOut) { _, signedOut in + if signedOut { onSignOut() } + } + .task { + await viewModel.loadUserInfo() + } + } + } + + // MARK: - Profile Section + + @ViewBuilder + private var profileSection: some View { + if viewModel.isLoadingUserInfo { + ProgressView("Loading profile…") + .frame(maxWidth: .infinity) + .padding(32) + } else if let errorMessage = viewModel.userInfoError { + Text(errorMessage) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding() + } else if !viewModel.userInfo.isEmpty { + UserInfoCard(userInfo: viewModel.userInfo) + } + } + + private var signOutButton: some View { + Button("Sign Out") { + Task { await viewModel.signOut() } + } + .buttonStyle(PingDestructiveButtonStyle()) + } + + private var settingsToolbarItem: some ToolbarContent { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showSettings = true + } label: { + Image(systemName: "gear") + .foregroundColor(.pingRed) + } + } + } +} + +// MARK: - Authenticated Header + +private struct AuthenticatedHeaderView: View { + var body: some View { + ZStack { + LinearGradient( + colors: [.pingRed, .pingRedDark], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + VStack(spacing: 12) { + Image("Logo") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + + Image(systemName: "checkmark.shield.fill") + .font(.system(size: 36)) + .foregroundColor(.white.opacity(0.9)) + + Text("Authenticated") + .font(.system(size: 22, weight: .bold)) + .foregroundColor(.white) + } + .padding(.vertical, 32) + } + .ignoresSafeArea(edges: .top) + } +} + +// MARK: - User Info Card + +private struct UserInfoCard: View { + let userInfo: [String: Any] + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text("Profile") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.secondary) + .textCase(.uppercase) + .padding(.bottom, 10) + + VStack(spacing: 0) { + ForEach(userInfo.keys.sorted(), id: \.self) { key in + UserInfoRow(key: key, value: String(describing: userInfo[key] ?? "")) + } + } + .background(Color(.secondarySystemGroupedBackground)) + .cornerRadius(12) + } + } +} + +private struct UserInfoRow: View { + let key: String + let value: String + + var body: some View { + HStack(alignment: .top, spacing: 12) { + Text(key) + .font(.caption) + .fontWeight(.medium) + .foregroundColor(.secondary) + .frame(width: 110, alignment: .leading) + + Text(value) + .font(.caption) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 10) + Divider() + .padding(.leading, 16) + } +} + +// MARK: - Destructive Button Style + +struct PingDestructiveButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.headline) + .foregroundColor(.pingRed) + .padding() + .frame(maxWidth: .infinity) + .frame(height: 50) + .background( + RoundedRectangle(cornerRadius: 15) + .stroke(Color.pingRed, lineWidth: 1.5) + .opacity(configuration.isPressed ? 0.6 : 1) + ) + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/FidoAuthenticationCallbackView.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/FidoAuthenticationCallbackView.swift new file mode 100644 index 00000000..1b78969f --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/FidoAuthenticationCallbackView.swift @@ -0,0 +1,75 @@ +// +// FidoAuthenticationCallbackView.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingFido + +struct FidoAuthenticationCallbackView: View { + let callback: FidoAuthenticationCallback + let onNext: () -> Void + + @State private var errorMessage: String? + @State private var isAuthenticating = false + + var body: some View { + VStack(spacing: 20) { + FidoIconView(systemName: "touchid", tint: .pingRed) + + VStack(spacing: 6) { + Text("Biometric Authentication") + .font(.title2) + .fontWeight(.semibold) + + Text("Use your passkey to sign in securely.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + + if let errorMessage { + ErrorMessageView(message: errorMessage) + } + + Button(action: authenticate) { + if isAuthenticating { + ProgressView() + .tint(.white) + } else { + Text("Authenticate with Passkey") + } + } + .buttonStyle(PingPrimaryButtonStyle()) + .disabled(isAuthenticating) + } + } + + private func authenticate() { + Task { + isAuthenticating = true + defer { isAuthenticating = false } + + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first else { + errorMessage = "Unable to find active window." + return + } + + let result = await callback.authenticate(window: window) + + switch result { + case .success: + onNext() + case .failure(let error): + errorMessage = error.localizedDescription + onNext() + } + } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/FidoRegistrationCallbackView.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/FidoRegistrationCallbackView.swift new file mode 100644 index 00000000..5e4d37d0 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/FidoRegistrationCallbackView.swift @@ -0,0 +1,79 @@ +// +// FidoRegistrationCallbackView.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingFido + +struct FidoRegistrationCallbackView: View { + let callback: FidoRegistrationCallback + let onNext: () -> Void + + @State private var deviceName = UIDevice.current.name + @State private var errorMessage: String? + @State private var isRegistering = false + + var body: some View { + VStack(spacing: 20) { + FidoIconView(systemName: "faceid", tint: .pingRed) + + VStack(spacing: 6) { + Text("Register Passkey") + .font(.title2) + .fontWeight(.semibold) + + Text("Create a passkey for fast, secure biometric login.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + + PingTextField(placeholder: "Device Name", text: $deviceName) + + if let errorMessage { + ErrorMessageView(message: errorMessage) + } + + Button(action: register) { + if isRegistering { + ProgressView() + .tint(.white) + } else { + Text("Register with Passkey") + } + } + .buttonStyle(PingPrimaryButtonStyle()) + .disabled(isRegistering) + } + } + + private func register() { + Task { + isRegistering = true + defer { isRegistering = false } + + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first else { + errorMessage = "Unable to find active window." + return + } + + let name = deviceName.isEmpty ? nil : deviceName + let result = await callback.register(deviceName: name, window: window) + + switch result { + case .success: + onNext() + case .failure(let error): + errorMessage = error.localizedDescription + onNext() + } + } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/NameCallbackView.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/NameCallbackView.swift new file mode 100644 index 00000000..a82007f1 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/NameCallbackView.swift @@ -0,0 +1,26 @@ +// +// NameCallbackView.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingJourney + +struct NameCallbackView: View { + let callback: NameCallback + @State private var username = "" + + var body: some View { + PingTextField( + placeholder: callback.prompt.isEmpty ? "Username" : callback.prompt, + text: $username, + contentType: .username + ) + .onChange(of: username) { callback.name = username } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/PasswordCallbackView.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/PasswordCallbackView.swift new file mode 100644 index 00000000..1665e041 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Callbacks/PasswordCallbackView.swift @@ -0,0 +1,25 @@ +// +// PasswordCallbackView.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingJourney + +struct PasswordCallbackView: View { + let callback: PasswordCallback + @State private var password = "" + + var body: some View { + PingSecureField( + placeholder: callback.prompt.isEmpty ? "Password" : callback.prompt, + text: $password + ) + .onChange(of: password) { callback.password = password } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Components/Theme.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Components/Theme.swift new file mode 100644 index 00000000..2e76cb05 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/Components/Theme.swift @@ -0,0 +1,189 @@ +// +// Theme.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI + +extension Color { + static let pingRed = Color(red: 163.0 / 255.0, green: 19.0 / 255.0, blue: 0.0 / 255.0) + static let pingRedDark = Color(red: 0.6, green: 0.1, blue: 0.1) + static let pingTextField = Color(red: 220.0 / 255.0, green: 230.0 / 255.0, blue: 230.0 / 255.0) +} + +// MARK: - Primary Action Button + +struct PingPrimaryButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(Color.pingRed.opacity(configuration.isPressed ? 0.8 : 1)) + .cornerRadius(15) + .shadow(color: .black.opacity(0.2), radius: 10, x: 0, y: 4) + } +} + +// MARK: - Branded Header + +struct PingHeaderView: View { + var body: some View { + ZStack { + LinearGradient( + colors: [.pingRed, .pingRedDark], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + VStack(spacing: 12) { + Image("Logo") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + + Text("Passkeys Demo") + .font(.system(size: 24, weight: .bold)) + .foregroundColor(.white) + + Text("Secure biometric authentication") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white.opacity(0.85)) + } + .padding(.vertical, 32) + } + .ignoresSafeArea(edges: .top) + } +} + +// MARK: - Branded Text Field + +struct PingTextField: View { + let placeholder: String + @Binding var text: String + var contentType: UITextContentType? = nil + var autocapitalization: TextInputAutocapitalization = .never + + var body: some View { + TextField(placeholder, text: $text) + .textContentType(contentType) + .autocorrectionDisabled() + .textInputAutocapitalization(autocapitalization) + .padding(12) + .background( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.pingTextField, lineWidth: 1.5) + ) + } +} + +// MARK: - Branded Secure Field + +struct PingSecureField: View { + let placeholder: String + @Binding var text: String + @State private var isSecure = true + + var body: some View { + HStack { + Group { + if isSecure { + SecureField(placeholder, text: $text) + } else { + TextField(placeholder, text: $text) + } + } + .textContentType(.password) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + + Button { + isSecure.toggle() + } label: { + Image(systemName: isSecure ? "eye.slash" : "eye") + .foregroundColor(.pingRed) + .frame(width: 20, height: 20) + } + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.pingTextField, lineWidth: 1.5) + ) + } +} + +// MARK: - FIDO Icon View + +struct FidoIconView: View { + let systemName: String + let tint: Color + + var body: some View { + ZStack { + Circle() + .fill(tint.opacity(0.12)) + .frame(width: 88, height: 88) + + Image(systemName: systemName) + .font(.system(size: 40)) + .foregroundColor(tint) + } + } +} + +// MARK: - Error Message View + +struct ErrorMessageView: View { + let message: String + + var body: some View { + HStack(spacing: 6) { + Image(systemName: "exclamationmark.circle.fill") + .font(.caption) + Text(message) + .font(.caption) + .multilineTextAlignment(.leading) + } + .foregroundColor(.red) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 4) + } +} + +// MARK: - Loading Overlay + +struct LoadingOverlay: View { + let message: String + + init(_ message: String = "Loading…") { + self.message = message + } + + var body: some View { + ZStack { + Color.black.opacity(0.35) + .ignoresSafeArea() + + VStack(spacing: 12) { + ProgressView() + .progressViewStyle(.circular) + .tint(.white) + .scaleEffect(1.4) + + Text(message) + .font(.subheadline) + .foregroundColor(.white) + } + .padding(24) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16)) + } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/ContentView.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/ContentView.swift new file mode 100644 index 00000000..967410c8 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/ContentView.swift @@ -0,0 +1,84 @@ +// +// ContentView.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingOrchestrate +import PingJourney + +struct ContentView: View { + @State private var loginViewModel = LoginViewModel() + + var body: some View { + Group { + switch loginViewModel.node { + case let successNode as SuccessNode: + AuthenticatedView(successNode: successNode, onSignOut: loginViewModel.reset) + case let failureNode as FailureNode: + ErrorView(message: failureNode.cause.localizedDescription, onRetry: retry) + case let errorNode as ErrorNode: + ErrorView( + message: errorNode.message.isEmpty ? "An error occurred." : errorNode.message, + onRetry: retry + ) + case let continueNode as ContinueNode: + LoginView( + continueNode: continueNode, + isLoading: loginViewModel.isLoading, + onNext: { Task { await loginViewModel.next(continueNode: continueNode) } } + ) + default: + LoginView( + continueNode: nil, + isLoading: loginViewModel.isLoading, + onNext: { Task { await loginViewModel.start() } } + ) + } + } + .task { + await loginViewModel.start() + } + } + + private func retry() { + loginViewModel.reset() + Task { await loginViewModel.start() } + } +} + +// MARK: - Error View + +private struct ErrorView: View { + let message: String + let onRetry: () -> Void + + var body: some View { + VStack(spacing: 24) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 56)) + .foregroundColor(.pingRed) + + VStack(spacing: 8) { + Text("Something went wrong") + .font(.title3) + .fontWeight(.semibold) + + Text(message) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + + Button("Try Again", action: onRetry) + .buttonStyle(PingPrimaryButtonStyle()) + .padding(.horizontal, 40) + } + .padding(32) + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/ContinueNodeView.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/ContinueNodeView.swift new file mode 100644 index 00000000..92c90d54 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/ContinueNodeView.swift @@ -0,0 +1,61 @@ +// +// ContinueNodeView.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingJourney +import PingJourneyPlugin +import PingOrchestrate +import PingFido + +struct ContinueNodeView: View { + let node: ContinueNode + let onNext: () -> Void + + var body: some View { + VStack(spacing: 16) { + ForEach(node.callbacks, id: \.id) { callback in + callbackView(for: callback) + } + + if !hasSelfAdvancingCallback { + Button("Next", action: onNext) + .buttonStyle(PingPrimaryButtonStyle()) + } + } + } + + // FIDO callbacks advance the node themselves after the OS prompt completes, + // so we suppress the generic "Next" button when they are present. + private var hasSelfAdvancingCallback: Bool { + node.callbacks.contains { $0 is FidoRegistrationCallback || $0 is FidoAuthenticationCallback } + } + + @ViewBuilder + private func callbackView(for callback: any Callback) -> some View { + switch callback { + case let nameCallback as NameCallback: + NameCallbackView(callback: nameCallback) + case let passwordCallback as PasswordCallback: + PasswordCallbackView(callback: passwordCallback) + case let textOutputCallback as TextOutputCallback: + Text(textOutputCallback.message) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.vertical, 4) + case let fidoRegistrationCallback as FidoRegistrationCallback: + FidoRegistrationCallbackView(callback: fidoRegistrationCallback, onNext: onNext) + case let fidoAuthenticationCallback as FidoAuthenticationCallback: + FidoAuthenticationCallbackView(callback: fidoAuthenticationCallback, onNext: onNext) + default: + EmptyView() + } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/LoginView.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/LoginView.swift new file mode 100644 index 00000000..1a4c8bd5 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/LoginView.swift @@ -0,0 +1,64 @@ +// +// LoginView.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingJourney +import PingOrchestrate + +struct LoginView: View { + let continueNode: ContinueNode? + let isLoading: Bool + let onNext: () -> Void + + var body: some View { + ZStack { + ScrollView { + VStack(spacing: 0) { + PingHeaderView() + + VStack(spacing: 24) { + if let continueNode { + ContinueNodeView(node: continueNode, onNext: onNext) + } else { + SignInPromptView(onSignIn: onNext) + } + } + .padding(24) + } + } + + if isLoading { + LoadingOverlay("Signing in…") + } + } + } +} + +// MARK: - Sign In Prompt + +private struct SignInPromptView: View { + let onSignIn: () -> Void + + var body: some View { + VStack(spacing: 16) { + Text("Welcome back") + .font(.title2) + .fontWeight(.semibold) + + Text("Sign in with your credentials or use a passkey for biometric authentication.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + + Button("Sign In", action: onSignIn) + .buttonStyle(PingPrimaryButtonStyle()) + } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/SettingsView.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/SettingsView.swift new file mode 100644 index 00000000..8a83526c --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI/Views/SettingsView.swift @@ -0,0 +1,106 @@ +// +// SettingsView.swift +// PasskeysSwiftUI +// +// Copyright (c) 2026 Ping Identity Corporation. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingJourney +import PingOrchestrate + +struct SettingsView: View { + @Environment(\.dismiss) private var dismiss + @State private var registrationViewModel = RegistrationViewModel() + @State private var biometricsEnabled = UserDefaults.standard.bool(forKey: UserDefaultsKey.biometricsEnabled) + + var body: some View { + NavigationStack { + ZStack { + settingsForm + if registrationViewModel.isLoading { + LoadingOverlay("Registering passkey…") + } + } + .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Done") { dismiss() } + .foregroundColor(.pingRed) + } + } + .onChange(of: registrationViewModel.isRegistered) { _, registered in + if registered { dismiss() } + } + .onChange(of: registrationViewModel.isFailure) { _, isFailure in + if isFailure { biometricsEnabled = false } + } + } + } + + // MARK: - Settings Form + + private var settingsForm: some View { + Form { + biometricsSection + registrationSection + errorSection + } + .tint(.pingRed) + } + + private var biometricsSection: some View { + Section { + Toggle("Biometric / Passkey Login", isOn: $biometricsEnabled) + .onChange(of: biometricsEnabled) { _, isEnabled in + if isEnabled { + Task { await registrationViewModel.startRegistration() } + } else { + UserDefaults.standard.set(false, forKey: UserDefaultsKey.biometricsEnabled) + } + } + } header: { + Text("Authentication") + } footer: { + Text(biometricsEnabled + ? "Passkey login is enabled. You will be prompted with Face ID or Touch ID on the next sign-in." + : "Enable to register a passkey for biometric login.") + } + } + + @ViewBuilder + private var registrationSection: some View { + if let continueNode = registrationViewModel.node as? ContinueNode { + Section("Passkey Registration") { + ContinueNodeView(node: continueNode, onNext: { + Task { await registrationViewModel.next(continueNode: continueNode) } + }) + .padding(.vertical, 8) + } + } + } + + @ViewBuilder + private var errorSection: some View { + if let failureNode = registrationViewModel.node as? FailureNode { + Section { + Label(failureNode.cause.localizedDescription, systemImage: "xmark.circle.fill") + .font(.subheadline) + .foregroundColor(.red) + } + } else if let errorNode = registrationViewModel.node as? ErrorNode { + Section { + Label( + errorNode.message.isEmpty ? "An error occurred during registration." : errorNode.message, + systemImage: "xmark.circle.fill" + ) + .font(.subheadline) + .foregroundColor(.red) + } + } + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUIUITests/PasskeysSwiftUIUITests.swift b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUIUITests/PasskeysSwiftUIUITests.swift new file mode 100644 index 00000000..d14ba0d9 --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUIUITests/PasskeysSwiftUIUITests.swift @@ -0,0 +1,26 @@ +import XCTest + +final class PasskeysSwiftUIUITests: XCTestCase { + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + XCTAssertTrue(true) + } +} diff --git a/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/README.md b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/README.md new file mode 100644 index 00000000..89279f2a --- /dev/null +++ b/iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/README.md @@ -0,0 +1,82 @@ +

+ + Ping Identity Logo + +


+

+ +# Passkeys Sample App using Swift/SwiftUI + +Ping provides these iOS samples to help demonstrate SDK functionality/implementation. They are provided "as is" and are not official products of Ping and are not officially supported. + +### Integrate with PingAM/AIC using Journeys and Passkeys: + +An example iOS project written in Swift/SwiftUI using the Ping iOS SDK, showcasing how to protect an application using Journeys and Passkeys. Based on the ["Set up passwordless authentication with Passkeys"](https://docs.pingidentity.com/sdks/latest/sdks/use-cases/how-to-go-passwordless-with-passkeys.html) blog post. + +## Requirements + +- Xcode: Latest version recommended +- iOS 18.0 or higher +- A physical iOS device or simulator +- PingAM/AIC server configured with the following journeys: + - `Login` — standard username and password authentication + - `BlogWebAuthnRegistration` — passkey registration + - `BlogWebAuthnAuthentication` — passkey-based sign-in + +## Getting Started + +To try out this sample, perform the following steps: + +1. Configure Ping Services + Ensure you have a PingAM/AIC server set up with the required authentication journeys and an OAuth 2.0 application for native mobile apps. See the [documentation](https://docs.pingidentity.com/sdks/latest/sdks/serverconfiguration/pingone/create-oauth2-client.html) for details. + +2. Clone this repo: + + ``` + git clone https://github.com/ForgeRock/sdk-sample-apps.git + ``` + +3. Open the workspace in Xcode: + + ``` + open iOS/swiftui-journey-module-passkeys/PasskeysSwiftUI/PasskeysSwiftUI.xcworkspace + ``` + +4. Open `PasskeysSwiftUI/Config/AppConfiguration.swift` and update `ServerConfig` with your environment values: + + ```swift + static let serverUrl = "https://your-server.example.com/am" + static let realm = "alpha" + static let cookieName = "your-cookie-name" + static let clientId = "your-client-id" + static let redirectUri = "yourapp://callback" + static let discoveryEndpoint = "https://your-server.example.com/am/oauth2/alpha/.well-known/openid-configuration" + ``` + +5. Update the Associated Domains entitlement in `Config/PasskeysSwiftUI.entitlements` to match your server domain (required for the WebAuthn relying party ID): + + ```xml + com.apple.developer.associated-domains + + webcredentials:your-server.example.com + webcredentials:your-server.example.com?mode=develop + + ``` + +6. Build and run the app on a physical iOS device. + +## Features + +This sample demonstrates: + +- Journey-based authentication with callback handling (username/password) +- FIDO2/WebAuthn passkey registration via `FidoRegistrationCallback` +- FIDO2/WebAuthn passkey authentication via `FidoAuthenticationCallback` +- Conditional login flow — automatically uses passkey sign-in when a passkey is registered +- Session management and sign-out via the Journey SDK +- OIDC user info display after successful authentication + +## Additional Resources + +- Ping SDK Documentation: https://docs.pingidentity.com/sdks/latest/sdks/index.html +- Passwordless with Passkeys blog: https://docs.pingidentity.com/sdks/latest/sdks/use-cases/how-to-go-passwordless-with-passkeys.html