diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..6f72ffa
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,12 @@
+# JIRA Ticket
+
+[JIRA](https://bugster.forgerock.org/jira/browse/SDKS-xxx) "Jira ticket title..."
+
+# Description
+
+Briefly describe the change and any information that would help speedup the review and testing process.
+
+# Checklist:
+
+- [ ] I ran all unit tests and they pass
+- [ ] I added test case coverage for my changes
diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml
new file mode 100644
index 0000000..e40e208
--- /dev/null
+++ b/.github/workflows/build-and-test.yaml
@@ -0,0 +1,58 @@
+name: Build and Test
+
+on:
+ workflow_call:
+ secrets:
+ SLACK_WEBHOOK:
+ description: 'Slack Notifier Webhook'
+ required: true
+jobs:
+ build-and-test:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [macos-14, macos-14-large]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 20
+
+ steps:
+ # Clone the repo
+ - name: Clone the repository
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
+ fetch-depth: 0
+
+ # Get the architecture of the chip
+ - name: Check chip architecture
+ run: echo "CHIP_TYPE=$(uname -m)" >> $GITHUB_ENV
+
+ # Set target Xcode version. For more details and options see:
+ # https://github.com/actions/virtual-environments/blob/main/images/macos/macos-14-Readme.md
+ - name: Select Xcode
+ run: sudo xcode-select -switch /Applications/Xcode_15.4.app && /usr/bin/xcodebuild -version
+
+ # Run all tests
+ - name: Run tests
+ run: xcodebuild test -scheme PingTestHost -workspace SampleApps/Ping.xcworkspace -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 15 Pro Max,OS=17.5' -derivedDataPath DerivedData -enableCodeCoverage YES -resultBundlePath TestResults | xcpretty && exit ${PIPESTATUS[0]}
+
+ # Publish test results
+ - name: Publish test results
+ uses: kishikawakatsumi/xcresulttool@v1
+ with:
+ title: "Test Results ${{ matrix.os }} - ${{ env.CHIP_TYPE }}"
+ path: TestResults.xcresult
+ show-passed-tests: false
+ if: success() || failure()
+
+ # Send slack notification with result status
+ - uses: 8398a7/action-slack@v3
+ with:
+ mention: 'stoyan.petrov'
+ if_mention: 'failure,cancelled'
+ fields: repo,author,eventName,message,job,pullRequest,took
+ status: ${{ job.status }}
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
+ if: always()
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..192f6a1
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,27 @@
+name: CI
+
+# Trigger on push or pull request
+on:
+ pull_request:
+ types: [opened, reopened, synchronize]
+ push:
+ branches:
+ - master
+ - develop
+
+jobs:
+ # Build and run unit tests
+ build-and-test:
+ name: Build and test
+ uses: ./.github/workflows/build-and-test.yaml
+ secrets:
+ SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
+
+ # Run Mend CLI Scan
+ mend-cli-scan:
+ name: Mend CLI Scan
+ uses: ./.github/workflows/mend-cli-scan.yaml
+ secrets:
+ MEND_EMAIL: ${{ secrets.MEND_EMAIL }}
+ MEND_USER_KEY: ${{ secrets.MEND_USER_KEY }}
+ SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
diff --git a/.github/workflows/mend-cli-scan.yaml b/.github/workflows/mend-cli-scan.yaml
new file mode 100644
index 0000000..a37551a
--- /dev/null
+++ b/.github/workflows/mend-cli-scan.yaml
@@ -0,0 +1,104 @@
+name: Run Mend CLS Scan
+on:
+ workflow_call:
+ secrets:
+ MEND_EMAIL:
+ description: Mend email
+ required: true
+ MEND_USER_KEY:
+ description: Mend user key
+ required: true
+ SLACK_WEBHOOK:
+ description: Slack Notifier Incoming Webhook
+ required: true
+
+jobs:
+ mend-cli-scan:
+ runs-on: macos-14
+
+ steps:
+ # Clone the repo
+ - name: Clone the repository
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
+ fetch-depth: 0
+
+ # Set target Xcode version. For more details and options see:
+ # https://github.com/actions/virtual-environments/blob/main/images/macos/macos-12-Readme.md
+ - name: Select Xcode
+ run: sudo xcode-select -switch /Applications/Xcode_15.4.app && /usr/bin/xcodebuild -version
+
+ # Setup Mend CLI
+ - name: Download and cache the Mend CLI executable
+ id: cache-mend
+ uses: actions/cache@v3
+ env:
+ mend-cache-name: cache-mend-executable
+ with:
+ path: /usr/local/bin/mend
+ key: ${{ runner.os }}-${{ env.mend-cache-name }}-${{ hashFiles('/usr/local/bin/mend') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ env.mend-cache-name }}-
+
+ # Download Mend CLI if it's not cached...
+ - if: ${{ steps.cache-mend.outputs.cache-hit != 'true' }}
+ name: Download Mend CLI executable (cache miss...)
+ continue-on-error: true
+ run: |
+ echo "Download Mend CLI executable (cache miss...)"
+ curl https://downloads.mend.io/cli/darwin_amd64/mend -o /usr/local/bin/mend && chmod +x /usr/local/bin/mend
+
+ # Execute the Mend CLI scan
+ - name: Mend CLI Scan
+ env:
+ MEND_EMAIL: ${{secrets.MEND_EMAIL}}
+ MEND_USER_KEY: ${{secrets.MEND_USER_KEY}}
+ MEND_URL: ${{ vars.MEND_SERVER_URL }}
+ run: |
+ mend dep --no-color -s ${{ vars.MEND_PRODUCT_NAME }}//${{ vars.MEND_PROJECT_NAME }} -u > mend-scan-result.txt
+ echo "MEND_SCAN_URL=$(cat mend-scan-result.txt | grep -Eo '(http|https)://[a-zA-Z0-9./?!=_%:-\#]*')" >> $GITHUB_ENV
+ echo "MEND_SCAN_SUMMARY=$(cat mend-scan-result.txt | grep -Eoiw '(Detected [0-9]* vulnerabilities.*)')" >> $GITHUB_ENV
+ echo "MEND_CRITICAL_COUNT=$(cat mend-scan-result.txt | grep -Eoiw '(Detected [0-9]* vulnerabilities.*)' | grep -oi '[0-9]* Critical' | grep -o [0-9]*)" >> $GITHUB_ENV
+ echo "MEND_HIGH_COUNT=$(cat mend-scan-result.txt | grep -Eoiw '(Detected [0-9]* vulnerabilities.*)' | grep -oi '[0-9]* High' | grep -o [0-9]*)" >> $GITHUB_ENV
+
+ # Check for failures and set the outcome of the workflow
+ - name: Parse the result and set job status
+ if: always()
+ run: |
+ if [ '${{ env.MEND_CRITICAL_COUNT }}' -gt '0' ] || [ '${{ env.MEND_HIGH_COUNT }}' -gt '0' ]; then
+ exit 1
+ else
+ exit 0
+ fi
+
+ # Publish the result
+ - name: Mend Scan Result
+ uses: LouisBrunner/checks-action@v1.6.1
+ if: always()
+ with:
+ name: "Mend Scan Result"
+ token: ${{ secrets.GITHUB_TOKEN }}
+ conclusion: ${{ job.status }}
+ output_text_description_file: mend-scan-result.txt
+ output: |
+ {"title":"Mend Scan Result", "summary":"${{ job.status }}"}
+
+ # Send slack notification with result status
+ - name: Send slack notification
+ uses: 8398a7/action-slack@v3
+ with:
+ status: custom
+ fields: all
+ custom_payload: |
+ {
+ attachments: [{
+ title: 'ForgeRock iOS SDK Mend Scan',
+ color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
+ text: `\nStatus: ${{ job.status }}\nWorkflow: ${process.env.AS_WORKFLOW} -> ${process.env.AS_JOB}\nSummary: ${{ env.MEND_SCAN_SUMMARY }}\nScan URL: ${{ env.MEND_SCAN_URL }}`,
+ }]
+ }
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
+ if: always()
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a60fa2a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,76 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Third-party dependency
+FRGoogleSignIn/FRGoogleSignIn/framework/*
+
+## Build generated
+build/
+DerivedData/
+docs/*
+SDKs/*
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+*.xcworkspacedata
+xcuserdata/
+
+
+## Obj-C/Swift specific
+*.hmap
+*.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
+.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/
+
+# 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
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3119f16
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+## [1.0.0]
+- General Availability release of the Ping SDK for iOS
+
+#### Added
+- Added Logger initial version
+- Added Storage initial version
+- Added Oidc initial version
+- Added Orchestrate initial version
+- Added Davinci initial version
diff --git a/Davinci/Davinci.xcodeproj/project.pbxproj b/Davinci/Davinci.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..bfa550f
--- /dev/null
+++ b/Davinci/Davinci.xcodeproj/project.pbxproj
@@ -0,0 +1,644 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 63;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 3A203D7F2BDB136C0020C995 /* DaVinci.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D7E2BDB136C0020C995 /* DaVinci.swift */; };
+ 3A203D972BDB2CD70020C995 /* Oidc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D962BDB2CD70020C995 /* Oidc.swift */; };
+ 3A203D9B2BDE155D0020C995 /* Transform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D9A2BDE155D0020C995 /* Transform.swift */; };
+ 3A292C822BF58462006977EF /* Agent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A292C812BF58462006977EF /* Agent.swift */; };
+ 3A5441AA2BCDF20700385131 /* PingDavinci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5441A12BCDF20700385131 /* PingDavinci.framework */; };
+ 3A5441AF2BCDF20700385131 /* DaVinciTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441AE2BCDF20700385131 /* DaVinciTests.swift */; };
+ 3A5441B02BCDF20700385131 /* Davinci.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A5441A42BCDF20700385131 /* Davinci.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 3A57CB622C44D68C001EC9A0 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A57CB612C44D688001EC9A0 /* User.swift */; };
+ 3AC13E332C3FFF1000DEF23A /* CollectorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC13E2F2C3FFF1000DEF23A /* CollectorFactory.swift */; };
+ 3AC13E342C3FFF1000DEF23A /* Collector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC13E302C3FFF1000DEF23A /* Collector.swift */; };
+ 3AC13E352C3FFF1000DEF23A /* Form.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC13E322C3FFF1000DEF23A /* Form.swift */; };
+ 3AC13E362C3FFF1000DEF23A /* Connector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC13E312C3FFF1000DEF23A /* Connector.swift */; };
+ A50981C22CEBDF2600F4B487 /* PingOidc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A50981C12CEBDF2600F4B487 /* PingOidc.framework */; };
+ A50981C32CEBDF2600F4B487 /* PingOidc.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A50981C12CEBDF2600F4B487 /* PingOidc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A51D4CD12C585B4500FE09E0 /* FieldCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD02C585B4500FE09E0 /* FieldCollector.swift */; };
+ A51D4CD32C585D7C00FE09E0 /* FlowCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD22C585D7C00FE09E0 /* FlowCollector.swift */; };
+ A51D4CD52C585DCD00FE09E0 /* PasswordCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD42C585DCD00FE09E0 /* PasswordCollector.swift */; };
+ A51D4CD72C585E2D00FE09E0 /* SubmitCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD62C585E2D00FE09E0 /* SubmitCollector.swift */; };
+ A51D4CD92C585E5700FE09E0 /* TextCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CD82C585E5700FE09E0 /* TextCollector.swift */; };
+ A51D4CDF2C599BFF00FE09E0 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CDE2C599BFF00FE09E0 /* Request.swift */; };
+ A51D4CE52C656A0C00FE09E0 /* CollectorRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CE42C656A0C00FE09E0 /* CollectorRegistryTests.swift */; };
+ A51D4CEE2C656EE100FE09E0 /* FieldCollectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CED2C656EE100FE09E0 /* FieldCollectorTests.swift */; };
+ A51D4CF02C656FC600FE09E0 /* MockResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CEF2C656FC600FE09E0 /* MockResponse.swift */; };
+ A51D4CF22C65729A00FE09E0 /* DaVinciErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF12C65729A00FE09E0 /* DaVinciErrorTests.swift */; };
+ A51D4CF42C6572E600FE09E0 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF32C6572E600FE09E0 /* MockURLProtocol.swift */; };
+ A51D4CF62C65743100FE09E0 /* MockAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF52C65743100FE09E0 /* MockAPIEndpoint.swift */; };
+ A51D4CF82C65978300FE09E0 /* DaVinciIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF72C65978300FE09E0 /* DaVinciIntegrationTests.swift */; };
+ A51D4CFA2C6BA75000FE09E0 /* CallbackFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CF92C6BA75000FE09E0 /* CallbackFactoryTests.swift */; };
+ A5A712482CAC527200B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A712472CAC527200B7DD58 /* PrivacyInfo.xcprivacy */; };
+ A5CCE5E72C85E96B00349A5E /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CCE5E62C85E96B00349A5E /* Constants.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 3A5441AB2BCDF20700385131 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3A5441982BCDF20700385131 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 3A5441A02BCDF20700385131;
+ remoteInfo = PingDavinci;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ A50981C42CEBDF2600F4B487 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ A50981C32CEBDF2600F4B487 /* PingOidc.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 3A203D7E2BDB136C0020C995 /* DaVinci.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaVinci.swift; sourceTree = ""; };
+ 3A203D962BDB2CD70020C995 /* Oidc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Oidc.swift; sourceTree = ""; };
+ 3A203D9A2BDE155D0020C995 /* Transform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transform.swift; sourceTree = ""; };
+ 3A292C812BF58462006977EF /* Agent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Agent.swift; sourceTree = ""; };
+ 3A5441A12BCDF20700385131 /* PingDavinci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingDavinci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3A5441A42BCDF20700385131 /* Davinci.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Davinci.h; sourceTree = ""; };
+ 3A5441A92BCDF20700385131 /* DavinciTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DavinciTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3A5441AE2BCDF20700385131 /* DaVinciTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaVinciTests.swift; sourceTree = ""; };
+ 3A57CB612C44D688001EC9A0 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
+ 3AC13E2F2C3FFF1000DEF23A /* CollectorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectorFactory.swift; sourceTree = ""; };
+ 3AC13E302C3FFF1000DEF23A /* Collector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collector.swift; sourceTree = ""; };
+ 3AC13E312C3FFF1000DEF23A /* Connector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connector.swift; sourceTree = ""; };
+ 3AC13E322C3FFF1000DEF23A /* Form.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Form.swift; sourceTree = ""; };
+ A50981C12CEBDF2600F4B487 /* PingOidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOidc.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A51D4CD02C585B4500FE09E0 /* FieldCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCollector.swift; sourceTree = ""; };
+ A51D4CD22C585D7C00FE09E0 /* FlowCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowCollector.swift; sourceTree = ""; };
+ A51D4CD42C585DCD00FE09E0 /* PasswordCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordCollector.swift; sourceTree = ""; };
+ A51D4CD62C585E2D00FE09E0 /* SubmitCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmitCollector.swift; sourceTree = ""; };
+ A51D4CD82C585E5700FE09E0 /* TextCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCollector.swift; sourceTree = ""; };
+ A51D4CDE2C599BFF00FE09E0 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; };
+ A51D4CE42C656A0C00FE09E0 /* CollectorRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectorRegistryTests.swift; sourceTree = ""; };
+ A51D4CED2C656EE100FE09E0 /* FieldCollectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCollectorTests.swift; sourceTree = ""; };
+ A51D4CEF2C656FC600FE09E0 /* MockResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockResponse.swift; sourceTree = ""; };
+ A51D4CF12C65729A00FE09E0 /* DaVinciErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaVinciErrorTests.swift; sourceTree = ""; };
+ A51D4CF32C6572E600FE09E0 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; };
+ A51D4CF52C65743100FE09E0 /* MockAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIEndpoint.swift; sourceTree = ""; };
+ A51D4CF72C65978300FE09E0 /* DaVinciIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaVinciIntegrationTests.swift; sourceTree = ""; };
+ A51D4CF92C6BA75000FE09E0 /* CallbackFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallbackFactoryTests.swift; sourceTree = ""; };
+ A5A712472CAC527200B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
+ A5CCE5E62C85E96B00349A5E /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 3A54419E2BCDF20700385131 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A50981C22CEBDF2600F4B487 /* PingOidc.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A5441A62BCDF20700385131 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3A5441AA2BCDF20700385131 /* PingDavinci.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 3A203D802BDB13FF0020C995 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ A50981C12CEBDF2600F4B487 /* PingOidc.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 3A5441972BCDF20700385131 = {
+ isa = PBXGroup;
+ children = (
+ 3A5441A32BCDF20700385131 /* Davinci */,
+ 3A5441AD2BCDF20700385131 /* DavinciTests */,
+ 3A5441A22BCDF20700385131 /* Products */,
+ 3A203D802BDB13FF0020C995 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 3A5441A22BCDF20700385131 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 3A5441A12BCDF20700385131 /* PingDavinci.framework */,
+ 3A5441A92BCDF20700385131 /* DavinciTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 3A5441A32BCDF20700385131 /* Davinci */ = {
+ isa = PBXGroup;
+ children = (
+ A5A712472CAC527200B7DD58 /* PrivacyInfo.xcprivacy */,
+ 3A5441A42BCDF20700385131 /* Davinci.h */,
+ A5CCE5E62C85E96B00349A5E /* Constants.swift */,
+ A51D4CE02C62C4BF00FE09E0 /* collector */,
+ A51D4CE12C62C53400FE09E0 /* module */,
+ 3A292C812BF58462006977EF /* Agent.swift */,
+ 3A203D7E2BDB136C0020C995 /* DaVinci.swift */,
+ 3A57CB612C44D688001EC9A0 /* User.swift */,
+ );
+ path = Davinci;
+ sourceTree = "";
+ };
+ 3A5441AD2BCDF20700385131 /* DavinciTests */ = {
+ isa = PBXGroup;
+ children = (
+ A509D1CE2C6D09C2003A0006 /* integration tests */,
+ A509D1CD2C6D06CA003A0006 /* mock */,
+ A51D4CF92C6BA75000FE09E0 /* CallbackFactoryTests.swift */,
+ A51D4CE42C656A0C00FE09E0 /* CollectorRegistryTests.swift */,
+ A51D4CF12C65729A00FE09E0 /* DaVinciErrorTests.swift */,
+ 3A5441AE2BCDF20700385131 /* DaVinciTests.swift */,
+ A51D4CED2C656EE100FE09E0 /* FieldCollectorTests.swift */,
+ );
+ path = DavinciTests;
+ sourceTree = "";
+ };
+ A509D1CD2C6D06CA003A0006 /* mock */ = {
+ isa = PBXGroup;
+ children = (
+ A51D4CEF2C656FC600FE09E0 /* MockResponse.swift */,
+ A51D4CF32C6572E600FE09E0 /* MockURLProtocol.swift */,
+ A51D4CF52C65743100FE09E0 /* MockAPIEndpoint.swift */,
+ );
+ path = mock;
+ sourceTree = "";
+ };
+ A509D1CE2C6D09C2003A0006 /* integration tests */ = {
+ isa = PBXGroup;
+ children = (
+ A51D4CF72C65978300FE09E0 /* DaVinciIntegrationTests.swift */,
+ );
+ path = "integration tests";
+ sourceTree = "";
+ };
+ A51D4CE02C62C4BF00FE09E0 /* collector */ = {
+ isa = PBXGroup;
+ children = (
+ 3AC13E302C3FFF1000DEF23A /* Collector.swift */,
+ 3AC13E2F2C3FFF1000DEF23A /* CollectorFactory.swift */,
+ A51D4CD02C585B4500FE09E0 /* FieldCollector.swift */,
+ A51D4CD22C585D7C00FE09E0 /* FlowCollector.swift */,
+ 3AC13E322C3FFF1000DEF23A /* Form.swift */,
+ A51D4CD42C585DCD00FE09E0 /* PasswordCollector.swift */,
+ A51D4CD62C585E2D00FE09E0 /* SubmitCollector.swift */,
+ A51D4CD82C585E5700FE09E0 /* TextCollector.swift */,
+ );
+ path = collector;
+ sourceTree = "";
+ };
+ A51D4CE12C62C53400FE09E0 /* module */ = {
+ isa = PBXGroup;
+ children = (
+ 3AC13E312C3FFF1000DEF23A /* Connector.swift */,
+ 3A203D962BDB2CD70020C995 /* Oidc.swift */,
+ A51D4CDE2C599BFF00FE09E0 /* Request.swift */,
+ 3A203D9A2BDE155D0020C995 /* Transform.swift */,
+ );
+ path = module;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+ 3A54419C2BCDF20700385131 /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3A5441B02BCDF20700385131 /* Davinci.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+ 3A5441A02BCDF20700385131 /* PingDavinci */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3A5441B32BCDF20700385131 /* Build configuration list for PBXNativeTarget "PingDavinci" */;
+ buildPhases = (
+ 3A54419C2BCDF20700385131 /* Headers */,
+ 3A54419D2BCDF20700385131 /* Sources */,
+ 3A54419E2BCDF20700385131 /* Frameworks */,
+ 3A54419F2BCDF20700385131 /* Resources */,
+ A50981C42CEBDF2600F4B487 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PingDavinci;
+ productName = PingDavinci;
+ productReference = 3A5441A12BCDF20700385131 /* PingDavinci.framework */;
+ productType = "com.apple.product-type.framework";
+ };
+ 3A5441A82BCDF20700385131 /* DavinciTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3A5441B62BCDF20700385131 /* Build configuration list for PBXNativeTarget "DavinciTests" */;
+ buildPhases = (
+ 3A5441A52BCDF20700385131 /* Sources */,
+ 3A5441A62BCDF20700385131 /* Frameworks */,
+ 3A5441A72BCDF20700385131 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 3A5441AC2BCDF20700385131 /* PBXTargetDependency */,
+ );
+ name = DavinciTests;
+ productName = PingDavinciTests;
+ productReference = 3A5441A92BCDF20700385131 /* DavinciTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 3A5441982BCDF20700385131 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1540;
+ TargetAttributes = {
+ 3A5441A02BCDF20700385131 = {
+ CreatedOnToolsVersion = 15.0;
+ LastSwiftMigration = 1500;
+ };
+ 3A5441A82BCDF20700385131 = {
+ CreatedOnToolsVersion = 15.0;
+ };
+ };
+ };
+ buildConfigurationList = 3A54419B2BCDF20700385131 /* Build configuration list for PBXProject "Davinci" */;
+ compatibilityVersion = "Xcode 15.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 3A5441972BCDF20700385131;
+ productRefGroup = 3A5441A22BCDF20700385131 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 3A5441A02BCDF20700385131 /* PingDavinci */,
+ 3A5441A82BCDF20700385131 /* DavinciTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 3A54419F2BCDF20700385131 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A712482CAC527200B7DD58 /* PrivacyInfo.xcprivacy in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A5441A72BCDF20700385131 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 3A54419D2BCDF20700385131 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A51D4CD12C585B4500FE09E0 /* FieldCollector.swift in Sources */,
+ A51D4CDF2C599BFF00FE09E0 /* Request.swift in Sources */,
+ A51D4CD32C585D7C00FE09E0 /* FlowCollector.swift in Sources */,
+ A51D4CD72C585E2D00FE09E0 /* SubmitCollector.swift in Sources */,
+ 3A292C822BF58462006977EF /* Agent.swift in Sources */,
+ 3A203D972BDB2CD70020C995 /* Oidc.swift in Sources */,
+ 3A203D9B2BDE155D0020C995 /* Transform.swift in Sources */,
+ A5CCE5E72C85E96B00349A5E /* Constants.swift in Sources */,
+ 3AC13E332C3FFF1000DEF23A /* CollectorFactory.swift in Sources */,
+ 3AC13E342C3FFF1000DEF23A /* Collector.swift in Sources */,
+ A51D4CD52C585DCD00FE09E0 /* PasswordCollector.swift in Sources */,
+ 3AC13E352C3FFF1000DEF23A /* Form.swift in Sources */,
+ 3AC13E362C3FFF1000DEF23A /* Connector.swift in Sources */,
+ A51D4CD92C585E5700FE09E0 /* TextCollector.swift in Sources */,
+ 3A57CB622C44D68C001EC9A0 /* User.swift in Sources */,
+ 3A203D7F2BDB136C0020C995 /* DaVinci.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A5441A52BCDF20700385131 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A51D4CEE2C656EE100FE09E0 /* FieldCollectorTests.swift in Sources */,
+ A51D4CFA2C6BA75000FE09E0 /* CallbackFactoryTests.swift in Sources */,
+ A51D4CF62C65743100FE09E0 /* MockAPIEndpoint.swift in Sources */,
+ A51D4CE52C656A0C00FE09E0 /* CollectorRegistryTests.swift in Sources */,
+ A51D4CF02C656FC600FE09E0 /* MockResponse.swift in Sources */,
+ A51D4CF42C6572E600FE09E0 /* MockURLProtocol.swift in Sources */,
+ A51D4CF82C65978300FE09E0 /* DaVinciIntegrationTests.swift in Sources */,
+ 3A5441AF2BCDF20700385131 /* DaVinciTests.swift in Sources */,
+ A51D4CF22C65729A00FE09E0 /* DaVinciErrorTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 3A5441AC2BCDF20700385131 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 3A5441A02BCDF20700385131 /* PingDavinci */;
+ targetProxy = 3A5441AB2BCDF20700385131 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 3A5441B12BCDF20700385131 /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ 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";
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Debug;
+ };
+ 3A5441B22BCDF20700385131 /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Release;
+ };
+ 3A5441B42BCDF20700385131 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingDavinci;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 3A5441B52BCDF20700385131 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingDavinci;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 3A5441B72BCDF20700385131 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingDavinciTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Debug;
+ };
+ 3A5441B82BCDF20700385131 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingDavinciTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 3A54419B2BCDF20700385131 /* Build configuration list for PBXProject "Davinci" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A5441B12BCDF20700385131 /* Debug */,
+ 3A5441B22BCDF20700385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 3A5441B32BCDF20700385131 /* Build configuration list for PBXNativeTarget "PingDavinci" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A5441B42BCDF20700385131 /* Debug */,
+ 3A5441B52BCDF20700385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 3A5441B62BCDF20700385131 /* Build configuration list for PBXNativeTarget "DavinciTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A5441B72BCDF20700385131 /* Debug */,
+ 3A5441B82BCDF20700385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 3A5441982BCDF20700385131 /* Project object */;
+}
diff --git a/Davinci/Davinci.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Davinci/Davinci.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/Davinci/Davinci.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Davinci/Davinci.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Davinci/Davinci.xcodeproj/xcshareddata/IDETemplateMacros.plist
new file mode 100644
index 0000000..16cf018
--- /dev/null
+++ b/Davinci/Davinci.xcodeproj/xcshareddata/IDETemplateMacros.plist
@@ -0,0 +1,17 @@
+
+
+
+
+ FILEHEADER
+
+// ___FILENAME___
+// ___PACKAGENAME___
+//
+// Copyright (c) ___YEAR___ Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+
diff --git a/Davinci/Davinci.xcodeproj/xcshareddata/xcschemes/Davinci.xcscheme b/Davinci/Davinci.xcodeproj/xcshareddata/xcschemes/Davinci.xcscheme
new file mode 100644
index 0000000..9f56ae2
--- /dev/null
+++ b/Davinci/Davinci.xcodeproj/xcshareddata/xcschemes/Davinci.xcscheme
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Davinci/Davinci/Agent.swift b/Davinci/Davinci/Agent.swift
new file mode 100644
index 0000000..d48f8cf
--- /dev/null
+++ b/Davinci/Davinci/Agent.swift
@@ -0,0 +1,59 @@
+//
+// Agent.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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 PingOidc
+import PingOrchestrate
+
+internal class CreateAgent: Agent {
+ typealias T = Void
+
+ let session: Session
+ let pkce: Pkce?
+ var used = false
+
+ init(session: Session, pkce: Pkce?) {
+ self.session = session
+ self.pkce = pkce
+ }
+
+ func config() -> () -> T {
+ return {}
+ }
+
+ func endSession(oidcConfig: OidcConfig, idToken: String) async throws -> Bool {
+ // Since we don't have the Session token, let DaVinci handle the sign-off
+ return true
+ }
+
+ func authorize(oidcConfig: OidcConfig) async throws -> AuthCode {
+ // We don't get the state; The state may not be returned since this is primarily for
+ // CSRF in redirect-based interactions, and pi.flow doesn't use redirect.
+ guard !session.value.isEmpty else {
+ throw OidcError.authorizeError(message: "Please start DaVinci flow to authenticate.")
+ }
+ guard !used else {
+ throw OidcError.authorizeError(message: "Auth code already used, please start DaVinci flow again.")
+ }
+
+ used = true
+ return session.authCode(pkce: pkce)
+ }
+
+}
+
+
+extension Session {
+ func authCode(pkce: Pkce?) -> AuthCode {
+ // parse the response and return the auth code
+ return AuthCode(code: value, codeVerifier: pkce?.codeVerifier)
+ }
+}
diff --git a/Davinci/Davinci/Constants.swift b/Davinci/Davinci/Constants.swift
new file mode 100644
index 0000000..dbf0e68
--- /dev/null
+++ b/Davinci/Davinci/Constants.swift
@@ -0,0 +1,50 @@
+//
+// Constants.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+enum Constants {
+ static let actionKey = "actionKey"
+ static let formData = "formData"
+ static let type = "type"
+ static let key = "key"
+ static let label = "label"
+ static let form = "form"
+ static let components = "components"
+ static let fields = "fields"
+ static let eventType = "eventType"
+ static let name = "name"
+ static let id = "id"
+ static let data = "data"
+ static let eventName = "eventName"
+ static let parameters = "parameters"
+ static let description = "description"
+ static let category = "category"
+ static let next = "next"
+ static let href = "href"
+ static let _links = "_links"
+ static let message = "message"
+ static let status = "status"
+ static let authorizeResponse = "authorizeResponse"
+ static let code = "code"
+ static let code_1999 = 1999
+ static let FAILED = "FAILED"
+ static let TEXT = "TEXT"
+ static let PASSWORD = "PASSWORD"
+ static let SUBMIT_BUTTON = "SUBMIT_BUTTON"
+ static let FLOW_BUTTON = "FLOW_BUTTON"
+ static let error = "error"
+ static let connectorId = "connectorId"
+ static let capabilityName = "capabilityName"
+ static let requestTimedOut = "requestTimedOut"
+ static let pingOneAuthenticationConnector = "pingOneAuthenticationConnector"
+ static let returnSuccessResponseRedirect = "returnSuccessResponseRedirect"
+ static let setSession = "setSession"
+ static let location = "Location"
+}
diff --git a/Davinci/Davinci/Davinci.h b/Davinci/Davinci/Davinci.h
new file mode 100644
index 0000000..2f3cb1a
--- /dev/null
+++ b/Davinci/Davinci/Davinci.h
@@ -0,0 +1,21 @@
+//
+// Davinci.h
+// Davinci
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+#import
+
+//! Project version number for Davinci.
+FOUNDATION_EXPORT double DavinciVersionNumber;
+
+//! Project version string for Davinci.
+FOUNDATION_EXPORT const unsigned char DavinciVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+
+
diff --git a/Davinci/Davinci/Davinci.swift b/Davinci/Davinci/Davinci.swift
new file mode 100644
index 0000000..db5a303
--- /dev/null
+++ b/Davinci/Davinci/Davinci.swift
@@ -0,0 +1,40 @@
+//
+// DaVinci.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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 PingOrchestrate
+import PingOidc
+
+public typealias DaVinci = Workflow
+public typealias DaVinciConfig = WorkflowConfig
+
+extension DaVinci {
+ /// Method to create a DaVinci instance.
+ /// - Parameter block: The configuration block.
+ /// - Returns: The DaVinci instance.
+ public static func createDaVinci(block: (DaVinciConfig) -> Void = {_ in }) -> DaVinci {
+ let config = DaVinciConfig()
+ config.module(CustomHeader.config) { customHeaderConfig in
+ customHeaderConfig.header(name: Request.Constants.xRequestedWith, value: Request.Constants.pingSdk)
+ customHeaderConfig.header(name: Request.Constants.xRequestedPlatform, value: Request.Constants.ios)
+ }
+ config.module(NodeTransformModule.config)
+ config.module(OidcModule.config)
+ config.module(CookieModule.config) { cookieConfig in
+ cookieConfig.persist = [Request.Constants.stCookie, Request.Constants.stNoSsCookie]
+ }
+
+ // Apply custom configuration
+ block(config)
+
+ return DaVinci(config: config)
+ }
+}
diff --git a/Davinci/Davinci/PrivacyInfo.xcprivacy b/Davinci/Davinci/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..fcfc9b9
--- /dev/null
+++ b/Davinci/Davinci/PrivacyInfo.xcprivacy
@@ -0,0 +1,10 @@
+
+
+
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTracking
+
+
+
diff --git a/Davinci/Davinci/User.swift b/Davinci/Davinci/User.swift
new file mode 100644
index 0000000..52f6c5e
--- /dev/null
+++ b/Davinci/Davinci/User.swift
@@ -0,0 +1,112 @@
+//
+// User.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import PingOidc
+import PingOrchestrate
+
+extension DaVinci {
+ /// Retrieve the user.
+ /// If cookies are available, it prepares a new user and returns it.
+ /// If no user is found and no cookies are available, it returns nil.
+ /// - Returns: The user if found, otherwise nil.
+ public func user() async -> User? {
+ try? await initialize()
+
+ if let cachedUser = self.sharedContext.get(key: SharedContext.Keys.userKey) as? User {
+ return cachedUser
+ }
+
+ if await hasCookies() {
+ if let oidcClientConfig = self.sharedContext.get(key: SharedContext.Keys.oidcClientConfigKey) as? OidcClientConfig {
+ return await prepareUser(daVinci: self, user: OidcUser(config: oidcClientConfig))
+ }
+ }
+ return nil
+ }
+
+ /// Alias for the DaVinci.user() method.
+ /// - Returns: The user if found, otherwise nil.
+ public func daVinciUser() async -> User? {
+ return await user()
+ }
+
+ /// Method to prepare the user.
+ /// This Method creates a new UserDelegate instance and caches it in the context.
+ /// - Parameters:
+ /// - daVinci: The DaVinci instance.
+ /// - user: The user.
+ /// - session: The session.
+ /// - Returns: The prepared user.
+ func prepareUser(
+ daVinci: DaVinci,
+ user: User,
+ session: Session = EmptySession()
+ ) async -> UserDelegate {
+ let userDelegate = UserDelegate(daVinci: daVinci, user: user, session: session)
+ // Cache the user in the context
+ self.sharedContext.set(key: SharedContext.Keys.userKey, value: userDelegate)
+ return userDelegate
+ }
+}
+
+
+extension SuccessNode {
+ /// Extension property for SuccessNode to cast the `SuccessNode.session` to a User.
+ public var user: User? {
+ return session as? User
+ }
+}
+
+
+/// Struct representing a UserDelegate.
+/// This struct is a delegate for the User and Session interfaces.
+/// It overrides the logout function to remove the cached user from the context and sign off the user.
+/// - property daVinci: The DaVinci instance.
+/// - property user: The user.
+/// - property session: The session.
+struct UserDelegate: User, Session {
+ private let daVinci: DaVinci
+ private let user: User
+ private let session: Session
+
+ init(daVinci: DaVinci, user: User, session: Session) {
+ self.daVinci = daVinci
+ self.user = user
+ self.session = session
+ }
+
+ /// Method to log out the user.
+ /// This method removes the cached user from the context and signs off the user.
+ func logout() async {
+ // remove the cached user from the context
+ _ = daVinci.sharedContext.removeValue(forKey: SharedContext.Keys.userKey)
+ // instead of calling `OidcClient.endSession` directly, we call `DaVinci.signOff` to sign off the user
+ _ = await daVinci.signOff()
+ }
+
+ func token() async -> Result {
+ return await user.token()
+ }
+
+ func revoke() async {
+ await user.revoke()
+ }
+
+ func userinfo(cache: Bool) async -> Result {
+ await user.userinfo(cache: cache)
+ }
+
+ var value: String {
+ get {
+ return session.value
+ }
+ }
+}
diff --git a/Davinci/Davinci/collector/Collector.swift b/Davinci/Davinci/collector/Collector.swift
new file mode 100644
index 0000000..2ec3b6f
--- /dev/null
+++ b/Davinci/Davinci/collector/Collector.swift
@@ -0,0 +1,77 @@
+//
+// FlowCollector.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+
+
+import PingOrchestrate
+import Foundation
+
+/// Protocol representing a Collector.
+public protocol Collector: Action, Identifiable {
+ var id: UUID { get }
+ init(with json: [String: Any])
+}
+
+
+extension ContinueNode {
+ /// Returns the list of collectors from the actions.
+ public var collectors: [any Collector] {
+ return actions.compactMap { $0 as? (any Collector) }
+ }
+}
+
+/// Type alias for a list of collectors.
+public typealias Collectors = [any Collector]
+
+
+extension Collectors {
+ /// Finds the event type from a list of collectors.
+ ///This function iterates over the list of collectors and returns the value if the collector's value is not empty.
+ /// - Returns: The event type as a String if found, otherwise nil.
+ func eventType() -> String? {
+ for collector in self {
+ if let submitCollector = collector as? SubmitCollector, !submitCollector.value.isEmpty {
+ return submitCollector.value
+ }
+ if let flowCollector = collector as? FlowCollector, !flowCollector.value.isEmpty {
+ return flowCollector.value
+ }
+ }
+ return nil
+ }
+
+ /// Represents a list of collectors as a JSON object for posting to the server.
+ /// This function takes a list of collectors and represents it as a JSON object. It iterates over the list of collectors,
+ /// adding each collector's key and value to the JSON object if the collector's value is not empty.
+ /// - Returns: JSON object representing the list of collectors.
+ func asJson() -> [String: Any] {
+ var jsonObject: [String: Any] = [:]
+
+ for collector in self {
+ if let submitCollector = collector as? SubmitCollector, !submitCollector.value.isEmpty {
+ jsonObject[Constants.actionKey] = submitCollector.key
+ }
+ if let flowCollector = collector as? FlowCollector, !flowCollector.value.isEmpty {
+ jsonObject[Constants.actionKey] = flowCollector.key
+ }
+ }
+
+ var formData: [String: Any] = [:]
+ for collector in self {
+ if let textCollector = collector as? TextCollector, !textCollector.value.isEmpty {
+ formData[textCollector.key] = textCollector.value
+ }
+ if let passwordCollector = collector as? PasswordCollector, !passwordCollector.value.isEmpty {
+ formData[passwordCollector.key] = passwordCollector.value
+ }
+ }
+
+ jsonObject[Constants.formData] = formData
+ return jsonObject
+ }
+}
diff --git a/Davinci/Davinci/collector/CollectorFactory.swift b/Davinci/Davinci/collector/CollectorFactory.swift
new file mode 100644
index 0000000..3d47a5b
--- /dev/null
+++ b/Davinci/Davinci/collector/CollectorFactory.swift
@@ -0,0 +1,57 @@
+//
+// CollectorFactory.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import PingOrchestrate
+
+/// The CollectorFactory singleton is responsible for creating and managing Collector instances.
+/// It maintains a dictionary of collector creation functions, keyed by type.
+/// It also provides functions to register new types of collectors and to create collectors from a JSON array.
+public final class CollectorFactory {
+ // A dictionary to hold the collector creation functions.
+ var collectors: [String: any Collector.Type] = [:]
+
+ /// The shared instance of the CollectorFactory.
+ public static let shared = CollectorFactory()
+
+ init() {
+ register(type: Constants.TEXT, collector: TextCollector.self)
+ register(type: Constants.PASSWORD, collector: PasswordCollector.self)
+ register(type: Constants.SUBMIT_BUTTON, collector: SubmitCollector.self)
+ register(type: Constants.FLOW_BUTTON, collector: FlowCollector.self)
+ }
+
+ /// Registers a new type of Collector.
+ /// - Parameters:
+ /// - type: The type of the Collector.
+ /// - block: A function that creates a new instance of the Collector.
+ public func register(type: String, collector: any Collector.Type) {
+ collectors[type] = collector
+ }
+
+ /// Creates a list of Collector instances from an array of dictionaries.
+ /// Each dictionary should have a "type" field that matches a registered Collector type.
+ /// - Parameter array: The array of dictionaries to create the Collectors from.
+ /// - Returns: A list of Collector instances.
+ public func collector(from array: [[String: Any]]) -> Collectors {
+ var list: [any Collector] = []
+ for item in array {
+ if let type = item[Constants.type] as? String, let collectorType = collectors[type] {
+ list.append(collectorType.init(with: item))
+ }
+ }
+ return list
+ }
+
+ /// Resets the CollectorFactory by clearing all registered collectors.
+ public func reset() {
+ collectors.removeAll()
+ }
+}
diff --git a/Davinci/Davinci/collector/FieldCollector.swift b/Davinci/Davinci/collector/FieldCollector.swift
new file mode 100644
index 0000000..4042da8
--- /dev/null
+++ b/Davinci/Davinci/collector/FieldCollector.swift
@@ -0,0 +1,34 @@
+//
+// FieldCollector.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Abstract class representing a field collector.
+/// - property key: The key of the field collector.
+/// - property label The label of the field collector.
+/// - property value The value of the field collector. It's open for modification.
+/// - property id The UUID of the field collector.
+open class FieldCollector: Collector {
+ public var key: String = ""
+ public var label: String = ""
+ public var value: String = ""
+ public let id = UUID()
+
+ /// Initializes a new instance of `FieldCollector`.
+ public init() {}
+
+ /// Initializes a new instance of `FieldCollector`.
+ /// - Parameter json: The json to initialize from.
+ required public init(with json: [String: Any]) {
+ key = json[Constants.key] as? String ?? ""
+ label = json[Constants.label] as? String ?? ""
+ }
+}
diff --git a/Davinci/Davinci/collector/FlowCollector.swift b/Davinci/Davinci/collector/FlowCollector.swift
new file mode 100644
index 0000000..325a189
--- /dev/null
+++ b/Davinci/Davinci/collector/FlowCollector.swift
@@ -0,0 +1,17 @@
+//
+// FlowCollector.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Class representing a FlowCollector.
+/// This class inherits from the FieldCollector class and implements the Collector protocol.
+/// It is used to collect data in a flow.
+public class FlowCollector: FieldCollector {}
diff --git a/Davinci/Davinci/collector/Form.swift b/Davinci/Davinci/collector/Form.swift
new file mode 100644
index 0000000..a73b2ca
--- /dev/null
+++ b/Davinci/Davinci/collector/Form.swift
@@ -0,0 +1,31 @@
+//
+// Form.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Class that handles the parsing and JSON representation of collectors.
+/// This class provides functions to parse a JSON object into a list of collectors and to represent a list of collectors as a JSON object.
+class Form {
+ /// Parses a JSON object into a list of collectors.
+ /// This function takes a JSON object and extracts the "form" field. It then iterates over the "fields" array in the "components" object,
+ /// parsing each field into a collector and adding it to a list.
+ /// - Parameter json :The JSON object to parse.
+ /// - Returns: A list of collectors parsed from the JSON object.
+ static func parse(json: [String: Any]) -> Collectors {
+ var collectors = Collectors()
+ if let form = json[Constants.form] as? [String: Any],
+ let components = form[Constants.components] as? [String: Any],
+ let fields = components[Constants.fields] as? [[String: Any]] {
+ collectors = CollectorFactory().collector(from: fields)
+ }
+ return collectors
+ }
+}
diff --git a/Davinci/Davinci/collector/PasswordCollector.swift b/Davinci/Davinci/collector/PasswordCollector.swift
new file mode 100644
index 0000000..89ca5d5
--- /dev/null
+++ b/Davinci/Davinci/collector/PasswordCollector.swift
@@ -0,0 +1,29 @@
+//
+// PasswordCollector.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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 PingOrchestrate
+
+/// Class representing a PasswordCollector.
+/// This class inherits from the FieldCollector class and implements the Closeable and Collector protocols.
+/// It is used to collect password data.
+public class PasswordCollector: FieldCollector, Closeable {
+ /// A flag to determine whether to clear the password or not after submission.
+ public var clearPassword: Bool = true
+
+ /// Overrides the close function from the Closeable protocol.
+ /// It is used to clear the value of the password field when the collector is closed.
+ public func close() {
+ if clearPassword {
+ value = ""
+ }
+ }
+}
diff --git a/Davinci/Davinci/collector/SubmitCollector.swift b/Davinci/Davinci/collector/SubmitCollector.swift
new file mode 100644
index 0000000..8f19b91
--- /dev/null
+++ b/Davinci/Davinci/collector/SubmitCollector.swift
@@ -0,0 +1,17 @@
+//
+// SubmitCollector.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Class representing a TextCollector.
+/// This class inherits from the FieldCollector class and implements the Collector protocol.
+/// `SubmitCollector` is responsible for collecting and managing submission fields.
+public class SubmitCollector: FieldCollector {}
diff --git a/Davinci/Davinci/collector/TextCollector.swift b/Davinci/Davinci/collector/TextCollector.swift
new file mode 100644
index 0000000..5fc3d9e
--- /dev/null
+++ b/Davinci/Davinci/collector/TextCollector.swift
@@ -0,0 +1,17 @@
+//
+// TextCollector.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Class representing a TextCollector.
+/// This class inherits from the FieldCollector class and implements the Collector protocol.
+/// It is used to collect text data.
+public class TextCollector: FieldCollector {}
diff --git a/Davinci/Davinci/module/Connector.swift b/Davinci/Davinci/module/Connector.swift
new file mode 100644
index 0000000..bb677b9
--- /dev/null
+++ b/Davinci/Davinci/module/Connector.swift
@@ -0,0 +1,106 @@
+//
+// Connector.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+
+
+import PingOrchestrate
+
+extension ContinueNode {
+ /// Extension property to get the id of a Connector.
+ public var id: String {
+ return (self as? Connector)?.idValue ?? ""
+ }
+
+ /// Extension property to get the name of a Connector.
+ public var name: String {
+ return (self as? Connector)?.nameValue ?? ""
+ }
+
+ /// Extension property to get the description of a Connector.
+ public var description: String {
+ return (self as? Connector)?.descriptionValue ?? ""
+ }
+
+ /// Extension property to get the category of a Connector.
+ public var category: String {
+ return (self as? Connector)?.categoryValue ?? ""
+ }
+}
+
+
+/// Class representing a Connector.
+///- property context: The FlowContext of the ContinueNode.
+///- property davinci: The Davinci Flow of the ContinueNode.
+///- property input: The input JsonObject of the ContinueNode.
+///- property collectors: The collectors of the ContinueNode.
+class Connector: ContinueNode {
+
+ /// Initializer to create a new instance of Connector.
+ /// - Parameters:
+ /// - context: The FlowContext of the ContinueNode.
+ /// - davinci: The Davinci Flow of the ContinueNode.
+ /// - input: The input JsonObject of the ContinueNode.
+ /// - collectors: The collectors of the ContinueNode.
+ init(context: FlowContext, davinci: DaVinci, input: [String: Any], collectors: Collectors) {
+ super.init(context: context, workflow: davinci, input: input, actions: collectors)
+ }
+
+ /// Function to convert the connector to a dictionary.
+ /// - returns: The connector as a JsonObject.
+ private func asJson() -> [String: Any] {
+ var parameters: [String: Any] = [:]
+ if let eventType = collectors.eventType() {
+ parameters[Constants.eventType] = eventType
+ }
+ parameters[Constants.data] = collectors.asJson()
+
+ return [
+ Constants.id: (input[Constants.id] as? String) ?? "",
+ Constants.eventName: (input[Constants.eventName] as? String) ?? "",
+ Constants.parameters: parameters
+ ]
+ }
+
+ /// Lazy property to get the id of the connector.
+ lazy var idValue: String = {
+ return input[Constants.id] as? String ?? ""
+ }()
+
+ /// Lazy property to get the name of the connector.
+ lazy var nameValue: String = {
+ guard let form = input[Constants.form] as? [String: Any] else { return "" }
+ return form[Constants.name] as? String ?? ""
+ }()
+
+ /// Lazy property to get the description of the connector.
+ lazy var descriptionValue: String = {
+ guard let form = input[Constants.form] as? [String: Any] else { return "" }
+ return form[Constants.description] as? String ?? ""
+ }()
+
+ /// Lazy property to get the category of the connector.
+ lazy var categoryValue: String = {
+ guard let form = input[Constants.form] as? [String: Any] else { return "" }
+ return form[Constants.category] as? String ?? ""
+ }()
+
+ /// Function to convert the connector to a Request.
+ /// - Returns: The connector as a Request.
+ override func asRequest() -> Request {
+ let request = Request()
+
+ let links: [String: Any]? = input[Constants._links] as? [String: Any]
+ let next = links?[Constants.next] as? [String: Any]
+ let href = next?[Constants.href] as? String ?? ""
+
+ request.url(href)
+ request.header(name: Request.Constants.contentType, value: Request.ContentType.json.rawValue)
+ request.body(body: asJson())
+ return request
+ }
+}
diff --git a/Davinci/Davinci/module/Oidc.swift b/Davinci/Davinci/module/Oidc.swift
new file mode 100644
index 0000000..28292f3
--- /dev/null
+++ b/Davinci/Davinci/module/Oidc.swift
@@ -0,0 +1,89 @@
+//
+// OIDC.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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 PingOidc
+import PingOrchestrate
+
+/// A module that integrates OIDC capabilities into the DaVinci workflow.
+public class OidcModule {
+
+ /// Initializes a new instance of `OidcModule`.
+ public init() {}
+
+ /// The configuration for the OIDC module.
+ public static let config: Module = Module.of ({ OidcClientConfig() }) { setup in
+
+ let config: OidcClientConfig = setup.config
+ let daVinciFlow: DaVinci = setup.workflow
+
+ // Initializes the module.
+ setup.initialize {
+ // propagate the configuration from workflow to the module
+ config.httpClient = daVinciFlow.config.httpClient
+ config.logger = daVinciFlow.config.logger
+ // global context
+ daVinciFlow.sharedContext.set(key: SharedContext.Keys.oidcClientConfigKey, value: config)
+ //Override the agent setting
+ config.updateAgent(DefaultAgent())
+ try await config.oidcInitialize()
+ }
+
+ // Starts the module.
+ setup.start { context, request in
+ // When user starts the flow again, revoke previous token if exists
+ await daVinciFlow.user()?.revoke()
+
+ let pkce = Pkce.generate()
+ context.flowContext.set(key: SharedContext.Keys.pkceKey, value: pkce)
+ return config.populateRequest(request: request, pkce: pkce)
+ }
+
+ // Handles success of the module.
+ setup.success { context, success in
+ let cloneConfig: OidcClientConfig = config.clone()
+
+ let flowPkce = context.flowContext.get(key: SharedContext.Keys.pkceKey) as? Pkce
+ let agent = CreateAgent(session: success.session, pkce: flowPkce)
+ cloneConfig.updateAgent(agent)
+
+ let oidcuser: User = OidcUser(config: cloneConfig)
+ let prepareUser = UserDelegate(daVinci: daVinciFlow, user: oidcuser, session: success.session)
+ daVinciFlow.sharedContext.set(key: SharedContext.Keys.userKey, value: prepareUser)
+
+ return SuccessNode(input: success.input, session: prepareUser)
+ }
+
+ // Handles sign off of the module.
+ setup.signOff { request in
+ request.url(config.openId?.endSessionEndpoint ?? "")
+
+ _ = await OidcClient(config: config).endSession { idToken in
+ request.parameter(name: OidcClient.Constants.id_token_hint, value: idToken)
+ request.parameter(name: OidcClient.Constants.client_id, value: config.clientId)
+ return true
+ }
+
+ return request
+ }
+ }
+}
+
+extension SharedContext.Keys {
+ /// The key used to store the PKCE value in the shared context.
+ public static let pkceKey = "com.pingidentity.davinci.PKCE"
+
+ /// The key used to store the user in the shared context.
+ public static let userKey = "com.pingidentity.davinci.User"
+
+ /// The key used to store the OIDC client configuration in the shared context.
+ public static let oidcClientConfigKey = "com.pingidentity.davinci.OidcClientConfig"
+}
diff --git a/Davinci/Davinci/module/Request.swift b/Davinci/Davinci/module/Request.swift
new file mode 100644
index 0000000..b774436
--- /dev/null
+++ b/Davinci/Davinci/module/Request.swift
@@ -0,0 +1,76 @@
+//
+// Request.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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 PingOidc
+import PingOrchestrate
+
+extension OidcClientConfig {
+ internal func populateRequest(
+ request: Request,
+ pkce: Pkce
+ ) -> Request {
+ request.url(openId?.authorizationEndpoint ?? "")
+ request.parameter(name: OidcClient.Constants.response_mode, value: "pi.flow")
+ request.parameter(name: OidcClient.Constants.client_id, value: clientId)
+ request.parameter(name: OidcClient.Constants.response_type, value: OidcClient.Constants.code)
+ request.parameter(name: OidcClient.Constants.scope, value: scopes.joined(separator: " "))
+ request.parameter(name: OidcClient.Constants.redirect_uri, value: redirectUri)
+ request.parameter(name: OidcClient.Constants.code_challenge, value: pkce.codeChallenge)
+ request.parameter(name: OidcClient.Constants.code_challenge_method, value: pkce.codeChallengeMethod)
+
+ if let acr = acrValues {
+ request.parameter(name: OidcClient.Constants.acr_values, value: acr)
+ }
+
+ if let display = display {
+ request.parameter(name: OidcClient.Constants.display, value: display)
+ }
+
+ for (key, value) in additionalParameters {
+ request.parameter(name: key, value: value)
+ }
+
+ if let loginHint = loginHint {
+ request.parameter(name: OidcClient.Constants.login_hint, value: loginHint)
+ }
+
+ if let nonce = nonce {
+ request.parameter(name: OidcClient.Constants.nonce, value: nonce)
+ }
+
+ if let prompt = prompt {
+ request.parameter(name: OidcClient.Constants.prompt, value: prompt)
+ }
+
+ if let uiLocales = uiLocales {
+ request.parameter(name: OidcClient.Constants.ui_locales, value: uiLocales)
+ }
+
+ return request
+ }
+}
+
+
+extension OidcClient.Constants {
+ static let response_mode = "response_mode"
+ static let response_type = "response_type"
+ static let scope = "scope"
+ static let code_challenge = "code_challenge"
+ static let code_challenge_method = "code_challenge_method"
+ static let acr_values = "acr_values"
+ static let display = "display"
+ static let nonce = "nonce"
+ static let prompt = "prompt"
+ static let ui_locales = "ui_locales"
+ static let login_hint = "login_hint"
+ static let piflow = "pi.flow"
+}
diff --git a/Davinci/Davinci/module/Transform.swift b/Davinci/Davinci/module/Transform.swift
new file mode 100644
index 0000000..e70f633
--- /dev/null
+++ b/Davinci/Davinci/module/Transform.swift
@@ -0,0 +1,123 @@
+//
+// Transform.swift
+// PingDavinci
+//
+// Copyright (c) 2024 Ping Identity. 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 PingOidc
+import PingOrchestrate
+
+/// Module for transforming the response from DaVinci to `Node`.
+public class NodeTransformModule {
+
+ /// The module configuration for transforming the response from DaVinci to `Node`.
+ public static let config: Module = Module.of(setup: { setup in
+ setup.transform { flowContext, response in
+ let status = response.status()
+
+ let body = response.body()
+
+ // Check for 4XX errors that are unrecoverable
+ if (400..<500).contains(status) {
+ let json = try response.json(data: response.data)
+ let message = json[Constants.message] as? String ?? ""
+
+ // Filter out client-side "timeout" related unrecoverable failures
+ if json[Constants.code] as? Int == Constants.code_1999 || json[Constants.code] as? String == Constants.requestTimedOut {
+ return FailureNode(cause: ApiError.error(status, json, body))
+ }
+
+ // Filter our "PingOne Authentication Connector" unrecoverable failures
+ if let connectorId = json[Constants.connectorId] as? String, connectorId == Constants.pingOneAuthenticationConnector,
+ let capabilityName = json[Constants.capabilityName] as? String,
+ [Constants.returnSuccessResponseRedirect, Constants.setSession].contains(capabilityName) {
+ return FailureNode(cause: ApiError.error(status, json, body))
+ }
+
+ // If we're still here, we have a 4XX failure that should be recoverable
+ return ErrorNode(status: status, input: json, message: message)
+ }
+
+ // Handle success (2XX) responses
+ if status == 200 {
+ let json = try response.json(data: response.data)
+
+ // Filter out 2XX errors with 'failure' status
+ if let failedStatus = json[Constants.status] as? String, failedStatus == Constants.FAILED {
+ return FailureNode(cause: ApiError.error(status, json, body))
+ }
+
+ // Filter out 2XX errors with error object
+ if let error = json[Constants.error] as? [String: Any], !error.isEmpty {
+ return FailureNode(cause: ApiError.error(status, json, body))
+ }
+
+ return transform(context: flowContext, davinci: setup.workflow, json: json)
+ }
+
+ // Handle success (3XX) responses
+ if (300..<400).contains(status) {
+ let locationHeader = response.header(name: Constants.location) ?? ""
+ return FailureNode(cause: ApiError.error(status, [:], "Location: \(String(describing: locationHeader))" ))
+ }
+
+ // 5XX errors are treated as unrecoverable failures
+ let json = try response.json(data: response.data)
+ return FailureNode(cause: ApiError.error(status, json, body))
+ }
+
+ })
+
+ private static func transform(context: FlowContext, davinci: DaVinci, json: [String: Any]) -> Node {
+ // If authorizeResponse is present, return success
+ if let _ = json[Constants.authorizeResponse] as? [String: Any] {
+ return SuccessNode(input: json, session: SessionResponse(json: json))
+ }
+
+ var collectors: Collectors = []
+ if let _ = json[Constants.form] {
+ collectors.append(contentsOf: Form.parse(json: json))
+ }
+
+ return Connector(context: context, davinci: davinci, input: json, collectors: collectors)
+ }
+}
+
+
+/// Represents a session response parsed from a JSON object.
+public struct SessionResponse: Session {
+ /// The raw JSON data of the session response.
+ public let json: [String: Any]
+
+ /// Initializes a new session response with the given JSON data.
+ /// - Parameter json: The JSON data representing the session response.
+ public init(json: [String: Any] = [:]) {
+ self.json = json
+ }
+
+ /// The session value extracted from the JSON response.
+ /// - Returns: A string representing the session code or an empty string if not available.
+ public var value: String {
+ get {
+ let authResponse = json[Constants.authorizeResponse] as? [String: Any]
+ return authResponse?[Constants.code] as? String ?? ""
+ }
+ }
+}
+
+
+/// Represents API errors that occur during response transformation.
+public enum ApiError: Error {
+ /// An error containing an HTTP status code, a JSON object, and a descriptive message.
+ /// - Parameters:
+ /// - status: The HTTP status code of the error.
+ /// - json: The JSON data associated with the error.
+ /// - message: A descriptive message explaining the error.
+ case error(Int, [String: Any], String)
+}
diff --git a/Davinci/DavinciTests/CallbackFactoryTests.swift b/Davinci/DavinciTests/CallbackFactoryTests.swift
new file mode 100644
index 0000000..5b487f6
--- /dev/null
+++ b/Davinci/DavinciTests/CallbackFactoryTests.swift
@@ -0,0 +1,70 @@
+//
+// CallbackFactoryTests.swift
+// DavinciTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingDavinci
+
+class CallbackFactoryTests: XCTestCase {
+ override func setUp() {
+ CollectorFactory.shared.register(type: "type1", collector: DummyCallback.self)
+ CollectorFactory.shared.register(type: "type2", collector: Dummy2Callback.self)
+ }
+
+ func testShouldReturnListOfCollectorsWhenValidTypesAreProvided() {
+ let jsonArray: [[String: Any]] = [
+ ["type": "type1"],
+ ["type": "type2"]
+ ]
+
+ let callbacks = CollectorFactory.shared.collector(from: jsonArray)
+ XCTAssertEqual((callbacks[0] as? DummyCallback)?.value, "dummy")
+ XCTAssertEqual((callbacks[1] as? Dummy2Callback)?.value, "dummy2")
+
+ XCTAssertEqual(callbacks.count, 2)
+ }
+
+ func testShouldReturnEmptyListWhenNoValidTypesAreProvided() {
+ let jsonArray: [[String: Any]] = [
+ ["type": "invalidType"]
+ ]
+
+ let callbacks = CollectorFactory.shared.collector(from: jsonArray)
+
+ XCTAssertTrue(callbacks.isEmpty)
+ }
+
+ func testShouldReturnEmptyListWhenJsonArrayIsEmpty() {
+ let jsonArray: [[String: Any]] = []
+
+ let callbacks = CollectorFactory.shared.collector(from: jsonArray)
+
+ XCTAssertTrue(callbacks.isEmpty)
+ }
+}
+
+class DummyCallback: Collector {
+ var id: UUID = UUID()
+ var value: String?
+
+ required public init(with json: [String: Any]) {
+ value = "dummy"
+ }
+}
+
+class Dummy2Callback: Collector {
+ var id: UUID = UUID()
+ var value: String?
+
+ required public init(with json: [String: Any]) {
+ value = "dummy2"
+ }
+}
diff --git a/Davinci/DavinciTests/CollectorRegistryTests.swift b/Davinci/DavinciTests/CollectorRegistryTests.swift
new file mode 100644
index 0000000..76ce17f
--- /dev/null
+++ b/Davinci/DavinciTests/CollectorRegistryTests.swift
@@ -0,0 +1,57 @@
+//
+// CollectorRegistryTests.swift
+// DavinciTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingDavinci
+
+class CollectorRegistryTests: XCTestCase {
+
+ private var collectorFactory: CollectorFactory!
+
+ override func setUp() {
+ super.setUp()
+ collectorFactory = CollectorFactory()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ collectorFactory.reset()
+ }
+
+ func testShouldRegisterCollector() {
+ let jsonArray: [[String: Any]] = [
+ ["type": "TEXT"],
+ ["type": "PASSWORD"],
+ ["type": "SUBMIT_BUTTON"],
+ ["type": "FLOW_BUTTON"]
+ ]
+
+ let collectors = collectorFactory.collector(from: jsonArray)
+ XCTAssertTrue(collectors[0] is TextCollector)
+ XCTAssertTrue(collectors[1] is PasswordCollector)
+ XCTAssertTrue(collectors[2] is SubmitCollector)
+ XCTAssertTrue(collectors[3] is FlowCollector)
+ }
+
+ func testShouldIgnoreUnknownCollector() {
+ let jsonArray: [[String: Any]] = [
+ ["type": "TEXT"],
+ ["type": "PASSWORD"],
+ ["type": "SUBMIT_BUTTON"],
+ ["type": "FLOW_BUTTON"],
+ ["type": "UNKNOWN"]
+ ]
+
+ let collectors = collectorFactory.collector(from: jsonArray)
+ XCTAssertEqual(collectors.count, 4)
+ }
+}
diff --git a/Davinci/DavinciTests/DaVinciErrorTests.swift b/Davinci/DavinciTests/DaVinciErrorTests.swift
new file mode 100644
index 0000000..e7064a2
--- /dev/null
+++ b/Davinci/DavinciTests/DaVinciErrorTests.swift
@@ -0,0 +1,591 @@
+//
+// DaVinciErrorTests.swift
+// DavinciTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingOrchestrate
+@testable import PingStorage
+@testable import PingLogger
+@testable import PingOidc
+@testable import PingDavinci
+
+class DaVinciErrorTests: XCTestCase {
+
+ override func setUp() {
+ super.setUp()
+ MockURLProtocol.startInterceptingRequests()
+ _ = CollectorFactory()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ MockURLProtocol.stopInterceptingRequests()
+ }
+
+ func testDaVinciWellKnownEndpointFailedwith404() async throws {
+
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 404, httpVersion: nil, headerFields: MockResponse.headers)!, "Not Found".data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ let error = (node as! FailureNode).cause as! OidcError
+
+ switch error {
+ case .apiError(let code, _):
+ XCTAssertEqual(code, 404)
+ default:
+ XCTFail()
+ }
+
+ }
+
+ func testDaVinciAuthorizeEndpointFailedWith401() async throws {
+ let number = Int.random(in: 400 ..< 500)
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: number, httpVersion: nil, headerFields: MockResponse.headers)!, """
+ {
+ "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc",
+ "code": 1999,
+ "message": "Unauthorized!",
+ "errorMessage": "Unauthorized!",
+ }
+ """.data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ let failureNode = node as! FailureNode
+ let apiError = failureNode.cause as! ApiError
+ switch apiError {
+ case .error(let code, _, _):
+ XCTAssertTrue(code == number)
+ }
+ }
+
+ func testDaVinciInvalidSessionBetween400To499() async throws {
+ let number = Int.random(in: 400 ..< 500)
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: number, httpVersion: nil, headerFields: MockResponse.headers)!, """
+ {
+ "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc",
+ "connectorId": "pingOneAuthenticationConnector",
+ "capabilityName": "setSession",
+ "message": "Invalid Connector.",
+ }
+ """.data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ let failureNode = node as! FailureNode
+ let apiError = failureNode.cause as! ApiError
+ switch apiError {
+ case .error(let code, _, _):
+ XCTAssertTrue(code == number)
+ }
+ }
+
+ func testDaVinciInvalidConnectorBetween400To499() async throws {
+ let number = Int.random(in: 400 ..< 500)
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: number, httpVersion: nil, headerFields: MockResponse.headers)!, """
+ {
+ "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc",
+ "connectorId": "pingOneAuthenticationConnector",
+ "capabilityName": "returnSuccessResponseRedirect",
+ "message": "Invalid Connector.",
+ }
+ """.data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ let failureNode = node as! FailureNode
+ let apiError = failureNode.cause as! ApiError
+ switch apiError {
+ case .error(let code, _, _):
+ XCTAssertTrue(code == number)
+ }
+ }
+
+ func testDaVinciTimeOutBetween400To499() async throws {
+ let number = Int.random(in: 400 ..< 500)
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: number, httpVersion: nil, headerFields: MockResponse.headers)!, """
+ {
+ "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc",
+ "code": "requestTimedOut",
+ "message": "Request timed out. Please try again.",
+ }
+ """.data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ let failureNode = node as! FailureNode
+ let apiError = failureNode.cause as! ApiError
+ switch apiError {
+ case .error(let code, _, _):
+ XCTAssertTrue(code == number)
+ }
+ }
+
+
+ func testDaVinciAuthorizeEndpointFailedBetween400To499() async throws {
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.headers)!, """
+ {
+ "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc",
+ "code": "INVALID_REQUEST",
+ "message": "Invalid DV Flow Policy ID: Single_Factor"
+ }
+ """.data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is ErrorNode)
+ let errorNode = node as! ErrorNode
+ XCTAssertTrue(errorNode.input.description.contains("INVALID_REQUEST"))
+ }
+
+ func testDaVinciAuthorizeEndpointFailedWith500() async throws {
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 500, httpVersion: nil, headerFields: MockResponse.headers)!, """
+ {
+ "id": "7bbe285f-c0e0-41ef-8925-c5c5bb370acc",
+ "code": "INVALID_REQUEST",
+ "message": "Invalid DV Flow Policy ID: Single_Factor"
+ }
+ """.data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ let failureNode = node as! FailureNode
+ let apiError = failureNode.cause as! ApiError
+ switch apiError {
+ case .error(let code, _, _):
+ XCTAssertTrue(code == 500)
+ }
+
+ }
+
+ func testDaVinciAuthorizeEndpointFailedWithOKResponseButFailedStatusDuringTransform() async throws {
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, """
+ {
+ "environment": {
+ "id": "0c6851ed-0f12-4c9a-a174-9b1bf8b438ae"
+ },
+ "status": "FAILED",
+ "error": {
+ "code": "login_required",
+ "message": "The request could not be completed. There was an issue processing the request"
+ }
+ }
+ """.data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ let error = (node as! FailureNode).cause as! ApiError
+
+ switch error {
+ case .error( _, _, let message):
+ XCTAssertTrue(message.contains("login_required"))
+ }
+
+ }
+
+ func testDaVinciAuthorizeEndpointFailedWithOKResponseButErrorDuringTransform() async throws {
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, """
+ {
+ "environment": {
+ "id": "0c6851ed-0f12-4c9a-a174-9b1bf8b438ae"
+ },
+ "error": {
+ "code": "login_required",
+ "message": "The request could not be completed. There was an issue processing the request"
+ }
+ }
+ """.data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ let error = (node as! FailureNode).cause as! ApiError
+
+ switch error {
+ case .error( _, _, let message):
+ XCTAssertTrue(message.contains("login_required"))
+ }
+
+ }
+
+ func testDaVinciTransformFailed() async throws {
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.authorizeResponseHeaders)!, " Not a Json ".data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ }
+
+ func testDaVinciInvalidPassword() async throws {
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.customHTMLTemplate.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.customHTMLTemplate.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.customHTMLTemplateHeaders)!, MockResponse.customHTMLTemplateWithInvalidPassword)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.authorizeResponseHeaders)!, MockResponse.authorizeResponse)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ let connector = node as! ContinueNode
+ if let textCollector = connector.collectors[0] as? TextCollector {
+ textCollector.value = "My First Name"
+ }
+ if let passwordCollector = connector.collectors[1] as? PasswordCollector {
+ passwordCollector.value = "My Password"
+ }
+ if let submitCollector = connector.collectors[2] as? SubmitCollector {
+ submitCollector.value = "click me"
+ }
+
+ let next = await connector.next()
+
+ XCTAssertEqual((connector.collectors[1] as? PasswordCollector)?.value, "")
+
+ XCTAssertTrue(next is ErrorNode)
+ let errorNode = next as! ErrorNode
+ XCTAssertEqual(errorNode.message, "Invalid username and/or password")
+ XCTAssertTrue(errorNode.input.description.contains("The provided password did not match provisioned password"))
+ }
+
+ func testDaVinci3xxErrorWithLocationHeaderInResponse() async throws {
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.authorization.url.path:
+ var headers = MockResponse.authorizeResponseHeaders
+ headers["Location"] = "https://apps.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/signon/?error=test"
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 302, httpVersion: nil, headerFields: headers)!, " ".data(using: .utf8)!)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ let node = await daVinci.start()
+ XCTAssertTrue(node is FailureNode)
+ let failureNode = node as! FailureNode
+ let apiError = failureNode.cause as! ApiError
+ switch apiError {
+ case .error(let code, _, let message):
+ XCTAssertEqual(message, "Location: https://apps.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/signon/?error=test")
+ XCTAssertTrue(code == 302)
+ }
+ }
+}
diff --git a/Davinci/DavinciTests/DaVinciTests.swift b/Davinci/DavinciTests/DaVinciTests.swift
new file mode 100644
index 0000000..e30cbf1
--- /dev/null
+++ b/Davinci/DavinciTests/DaVinciTests.swift
@@ -0,0 +1,309 @@
+//
+// DaVinciTests.swift
+// DavinciTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingOrchestrate
+@testable import PingLogger
+@testable import PingOidc
+@testable import PingStorage
+@testable import PingDavinci
+
+final class DaVinciTests: XCTestCase {
+
+ override func setUp() {
+ super.setUp()
+ MockURLProtocol.startInterceptingRequests()
+ _ = CollectorFactory()
+
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfigurationResponse)
+ case MockAPIEndpoint.token.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenResponse)
+ case MockAPIEndpoint.userinfo.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.userinfo.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.userinfoResponse)
+ case MockAPIEndpoint.revocation.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.revocation.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, Data())
+ case MockAPIEndpoint.endSession.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.endSession.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, Data())
+ case MockAPIEndpoint.customHTMLTemplate.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.customHTMLTemplate.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.customHTMLTemplateHeaders)!, MockResponse.customHTMLTemplate)
+ case MockAPIEndpoint.authorization.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.authorization.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.authorizeResponseHeaders)!, MockResponse.authorizeResponse)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ MockURLProtocol.stopInterceptingRequests()
+ }
+
+ func testDaVinci() throws {
+
+ let davinci = DaVinci.createDaVinci { config in
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1"
+ oidcValue.scopes = ["openid", "email", "address", "phone", "profile"]
+ oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect"
+ oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration"
+ }
+ }
+
+ XCTAssertEqual(davinci.config.modules.count, 4)
+ XCTAssertEqual(davinci.initHandlers.count, 2)
+ XCTAssertEqual(davinci.nextHandlers.count, 2)
+ XCTAssertEqual(davinci.nodeHandlers.count, 0)
+ XCTAssertEqual(davinci.responseHandlers.count, 1)
+ XCTAssertEqual(davinci.signOffHandlers.count, 2)
+ XCTAssertEqual(davinci.successHandlers.count, 1)
+
+ let nosession = Module.of { setup in
+ setup.next { ( context,connector, request) in
+ request.header(name: "nosession", value: "true")
+ return request
+ }
+ }
+
+ let davinci1 = DaVinci.createDaVinci { config in
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1"
+ oidcValue.scopes = ["openid", "email", "address", "phone", "profile"]
+ oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect"
+ oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration"
+ }
+
+ config.module(nosession)
+ }
+
+ XCTAssertEqual(davinci1.config.modules.count, 5)
+ XCTAssertEqual(davinci1.initHandlers.count, 2)
+ XCTAssertEqual(davinci1.nextHandlers.count, 3)
+ XCTAssertEqual(davinci1.nodeHandlers.count, 0)
+ XCTAssertEqual(davinci1.responseHandlers.count, 1)
+ XCTAssertEqual(davinci1.signOffHandlers.count, 2)
+ XCTAssertEqual(davinci1.successHandlers.count, 1)
+ }
+
+
+ func testDaVinciDefaultModuleSequence() async throws {
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ XCTAssertEqual(4, daVinci.config.modules.count)
+ let list = daVinci.config.modules
+ XCTAssertTrue(list[0].config is CustomHeaderConfig)
+ XCTAssertTrue(list[1].config is Void)
+ XCTAssertTrue(list[2].config is OidcClientConfig)
+ XCTAssertTrue(list[3].config is CookieConfig)
+
+ }
+
+ func testDaVinciSimpleHappyPath() async throws {
+ let tokenStorage = MemoryStorage()
+ let cookieStorage = MemoryStorage<[CustomHTTPCookie]>()
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = tokenStorage
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = cookieStorage
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ let continueNode = node as! ContinueNode
+ XCTAssertEqual(continueNode.collectors.count, 5)
+
+ XCTAssertEqual(continueNode.id, "cq77vwelou")
+ XCTAssertEqual(continueNode.name, "Username/Password Form")
+ XCTAssertEqual(continueNode.description, "Test Description")
+ XCTAssertEqual(continueNode.category, "CUSTOM_HTML")
+
+ (continueNode.collectors[0] as? TextCollector)?.value = "My First Name"
+ (continueNode.collectors[1] as? PasswordCollector)?.value = "My Password"
+ (continueNode.collectors[2] as? SubmitCollector)?.value = "click me"
+
+ node = await continueNode.next()
+ XCTAssertTrue(node is SuccessNode)
+
+ let authorizeReq = MockURLProtocol.requestHistory[1]
+ XCTAssertTrue(authorizeReq.url!.query?.contains("client_id=test") ?? false)
+ XCTAssertTrue(authorizeReq.url!.query?.contains("response_mode=pi.flow") ?? false)
+ XCTAssertTrue(authorizeReq.url!.query?.contains("code_challenge_method=S256") ?? false)
+ XCTAssertTrue(authorizeReq.url!.query?.contains("code_challenge=") ?? false)
+ XCTAssertTrue(authorizeReq.url!.query?.contains("redirect_uri=") ?? false)
+
+ let request = MockURLProtocol.requestHistory[2]
+ //let result = request.httpBody as! TextContent
+ // let json = try JSONSerialization.jsonObject(with: request.httpBody!) as! [String: Any]
+ // XCTAssertEqual(json["eventName"] as? String, "continue")
+ // let parameters = json["parameters"] as! [String: Any]
+ // let data = parameters["data"] as! [String: Any]
+ // XCTAssertEqual(data["actionKey"] as? String, "SIGNON")
+ // let formData = data["formData"] as! [String: Any]
+ // XCTAssertEqual(formData["username"] as? String, "My First Name")
+ // XCTAssertEqual(formData["password"] as? String, "My Password")
+
+ XCTAssertEqual(request.allHTTPHeaderFields!["x-requested-with"], "ping-sdk")
+ XCTAssertEqual(request.allHTTPHeaderFields!["x-requested-platform"], "ios")
+ XCTAssertTrue(request.allHTTPHeaderFields!["Cookie"]?.contains("interactionId") ?? false)
+ //XCTAssertTrue(request.allHTTPHeaderFields!["Cookie"]?.contains("interactionToken") ?? false)
+ //XCTAssertTrue(request.allHTTPHeaderFields!["Cookie"]?.contains("skProxyApiEnvironmentId") ?? false)
+
+ let successNode = node as! SuccessNode
+ let user = successNode.user
+ let userToken = await user?.token()
+ switch userToken! {
+ case .success(let token):
+ XCTAssertEqual(token.accessToken, "Dummy AccessToken")
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+ // XCTAssertEqual((user?.token() as? Result.Success)?.value.accessToken, "Dummy AccessToken")
+
+ let u = await daVinci.user()
+ await u?.logout()
+ let revoke = MockURLProtocol.requestHistory[4]
+ XCTAssertEqual(revoke.url!.absoluteString, "https://auth.test-one-pingone.com/revoke")
+ // let revokeBody = try JSONSerialization.jsonObject(with: revoke.httpBody!) as! [String: Any]
+ // XCTAssertEqual(revokeBody["client_id"] as? String, "test")
+ // XCTAssertEqual(revokeBody["token"] as? String, "Dummy RefreshToken")
+
+ let signOff = MockURLProtocol.requestHistory[5]
+ XCTAssertEqual(signOff.url!.absoluteString, "https://auth.test-one-pingone.com/signoff?id_token_hint=Dummy%20IdToken&client_id=test")
+ XCTAssertTrue(signOff.allHTTPHeaderFields!["Cookie"]?.contains("ST=session_token") ?? false)
+
+ let storedToken = try await tokenStorage.get()
+ XCTAssertNil(storedToken)
+ let storedCokie = try await cookieStorage.get()
+ XCTAssertNil(storedCokie)
+ let storedUser = await daVinci.user()
+ XCTAssertNil(storedUser)
+ }
+
+ func testDaVinciAdditionOidcParameter() async throws {
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = MemoryStorage()
+ oidcValue.logger = LogManager.standard
+ oidcValue.acrValues = "acrValues"
+ oidcValue.display = "display"
+ oidcValue.loginHint = "login_hint"
+ oidcValue.nonce = "nonce"
+ oidcValue.prompt = "prompt"
+ oidcValue.uiLocales = "ui_locales"
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = MemoryStorage()
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ let connector = node as! ContinueNode
+ (connector.collectors[0] as? TextCollector)?.value = "My First Name"
+ (connector.collectors[1] as? PasswordCollector)?.value = "My Password"
+ (connector.collectors[2] as? SubmitCollector)?.value = "click me"
+
+ node = await connector.next()
+ XCTAssertTrue(node is SuccessNode)
+
+ let authorizeReq = MockURLProtocol.requestHistory[1]
+ XCTAssertTrue(authorizeReq.url?.query?.contains("client_id=test") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("response_mode=pi.flow") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("code_challenge_method=S256") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("code_challenge=") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("redirect_uri=http://localhost:8080") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("acr_values=acrValues") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("display=display") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("login_hint=login_hint") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("nonce=nonce") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("prompt=prompt") ?? false)
+ XCTAssertTrue(authorizeReq.url?.query?.contains("ui_locales=ui_locales") ?? false)
+ }
+
+ func testDaVinciRevokeAccessToken() async throws {
+ let tokenStorage = MemoryStorage()
+ let cookieStorage = MemoryStorage<[CustomHTTPCookie]>()
+ let daVinci = DaVinci.createDaVinci { config in
+ config.httpClient = HttpClient(session: .shared)
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "http://localhost:8080"
+ oidcValue.discoveryEndpoint = "http://localhost/.well-known/openid-configuration"
+ oidcValue.storage = tokenStorage
+ oidcValue.logger = LogManager.standard
+ }
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = cookieStorage
+ cookieValue.persist = ["ST"]
+ }
+ }
+
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ let connector = node as! ContinueNode
+ (connector.collectors[0] as? TextCollector)?.value = "My First Name"
+ (connector.collectors[1] as? PasswordCollector)?.value = "My Password"
+ (connector.collectors[2] as? SubmitCollector)?.value = "click me"
+
+ node = await connector.next()
+ XCTAssertTrue(node is SuccessNode)
+
+ let u = await daVinci.user()
+ await u?.revoke()
+ let storedToken = try await tokenStorage.get()
+ XCTAssertNil(storedToken)
+ let storedCokie = try await cookieStorage.get()
+ XCTAssertNotNil(storedCokie)
+ }
+}
diff --git a/Davinci/DavinciTests/FieldCollectorTests.swift b/Davinci/DavinciTests/FieldCollectorTests.swift
new file mode 100644
index 0000000..c5bb125
--- /dev/null
+++ b/Davinci/DavinciTests/FieldCollectorTests.swift
@@ -0,0 +1,38 @@
+//
+// FieldCollectorTests.swift
+// DavinciTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingDavinci
+
+class FieldCollectorTests: XCTestCase {
+
+ class MockFieldCollector: FieldCollector {}
+
+ func testShouldInitializeKeyAndLabelFromJsonObject() {
+
+ let jsonObject: [String: String] = [
+ "key": "testKey",
+ "label": "testLabel"
+ ]
+
+ let fieldCollector = MockFieldCollector(with: jsonObject)
+
+ XCTAssertEqual("testKey", fieldCollector.key)
+ XCTAssertEqual("testLabel", fieldCollector.label)
+ }
+
+ func testShouldReturnValueWhenValueIsSet() {
+ let fieldCollector = MockFieldCollector()
+ fieldCollector.value = "test"
+ XCTAssertEqual("test", fieldCollector.value)
+ }
+}
diff --git a/Davinci/DavinciTests/integration tests/DaVinciIntegrationTests.swift b/Davinci/DavinciTests/integration tests/DaVinciIntegrationTests.swift
new file mode 100644
index 0000000..4faccc5
--- /dev/null
+++ b/Davinci/DavinciTests/integration tests/DaVinciIntegrationTests.swift
@@ -0,0 +1,863 @@
+//
+// DaVinciIntegrationTests.swift
+// DavinciTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+import XCTest
+@testable import PingOrchestrate
+@testable import PingLogger
+@testable import PingOidc
+@testable import PingStorage
+@testable import PingDavinci
+
+class DaVinciIntegrationTests: XCTestCase {
+ private var daVinci: DaVinci!
+ private var username: String!
+ private var userFname: String!
+ private var userLname: String!
+ private var password: String!
+ private var newPassword: String!
+ private var verificationCode: String!
+
+ override func setUp() async throws {
+ try await super.setUp()
+
+ username = "e2euser@example.com"
+ userFname = "E2E"
+ userLname = "iOS"
+ password = "Demo1234#1"
+ newPassword = "New1234#1"
+ verificationCode = "1234"
+
+ daVinci = DaVinci.createDaVinci { config in
+ config.logger = LogManager.standard
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "021b83ce-a9b1-4ad4-8c1d-79e576eeab76"
+ oidcValue.scopes = ["openid", "email", "address", "phone", "profile"]
+ oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect"
+ oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration"
+ }
+ }
+
+ // Start with a clean session
+ await daVinci.user()?.logout()
+ }
+
+ // TestRailCase(21274)
+ func testLoginSuccess() async throws {
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ var continueNode = node as! ContinueNode
+
+ // Login form validation...
+ XCTAssertEqual(continueNode.collectors.count, 5)
+ XCTAssertTrue(continueNode.collectors[0] is TextCollector)
+ XCTAssertTrue(continueNode.collectors[1] is PasswordCollector)
+ XCTAssertTrue(continueNode.collectors[2] is SubmitCollector)
+ XCTAssertTrue(continueNode.collectors[3] is FlowCollector)
+ XCTAssertTrue(continueNode.collectors[4] is FlowCollector)
+
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+ XCTAssertEqual("Enter your username and password", continueNode.description)
+
+ (continueNode.collectors[0] as? TextCollector)?.value = username
+ (continueNode.collectors[1] as? PasswordCollector)?.value = password
+ (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" // This will submit the form...
+ node = await continueNode.next()
+ XCTAssertTrue(node is ContinueNode)
+ continueNode = node as! ContinueNode
+
+ // Verify the Successful login form
+ XCTAssertTrue(continueNode.collectors.count == 3)
+ XCTAssertTrue(continueNode.collectors[0] is SubmitCollector)
+ XCTAssertTrue(continueNode.collectors[1] is FlowCollector)
+ XCTAssertTrue(continueNode.collectors[2] is FlowCollector)
+
+ XCTAssertEqual("Successful login", continueNode.name)
+ XCTAssertEqual("Successfully logged in to DaVinci", continueNode.description)
+ XCTAssertEqual("Continue", (continueNode.collectors[0] as! SubmitCollector).label)
+ XCTAssertEqual("Reset password...", (continueNode.collectors[1] as! FlowCollector).label)
+ XCTAssertEqual("Delete user...", (continueNode.collectors[2] as! FlowCollector).label)
+
+ // Click continue
+ (continueNode.collectors[0] as! SubmitCollector).value = "Continue"
+
+ node = await continueNode.next()
+ XCTAssertTrue(node is SuccessNode)
+ let successNode = node as! SuccessNode
+
+ let user = successNode.user
+ let userToken = await user?.token()
+ switch userToken! {
+ case .success(let token):
+ XCTAssertNotNil(token.accessToken)
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ let u = await daVinci.user()
+ await u?.logout() ?? { XCTFail("User is null") }()
+
+ // After logout make sure the user is null
+ let daVinciUser = await daVinci.user()
+ XCTAssertNil(daVinciUser)
+ }
+
+ // TestRailCase(21275)
+ func testLoginFailure() async throws {
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ let continueNode = node as! ContinueNode
+
+ // Make sure that we are at the Login form...
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ (continueNode.collectors[0] as? TextCollector)?.value = username
+ (continueNode.collectors[1] as? PasswordCollector)?.value = "invalid"
+ (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On"
+ node = await continueNode.next()
+ XCTAssertTrue(node is ErrorNode)
+ let errorNode = node as! ErrorNode
+
+ // Verify the error message upon attempt to login with invalid credentials
+ XCTAssertEqual("Invalid username and/or password", errorNode.message)
+
+ let daVinciUser = await daVinci.user()
+ XCTAssertNil(daVinciUser)
+ }
+
+ // TestRailCase(21276)
+ func testActiveSession() async throws {
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ var continueNode = node as! ContinueNode
+
+ // Ensure that we are at the Login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Login with valid credentials...
+ (continueNode.collectors[0] as? TextCollector)?.value = username
+ (continueNode.collectors[1] as? PasswordCollector)?.value = password
+ (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // Verify the user was successfully logged in
+ XCTAssertEqual("Successful login", continueNode.name)
+
+ // Click continue
+ (continueNode.collectors[0] as! SubmitCollector).value = "Continue"
+
+ node = await continueNode.next()
+ XCTAssertTrue(node is SuccessNode)
+ var successNode = node as! SuccessNode
+
+ var user = successNode.user
+ var userToken = await user?.token()
+ switch userToken! {
+ case .success(let token):
+ XCTAssertNotNil(token.accessToken)
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ // Launch the login form again (active session exists...)
+ // Should go directly to success...
+ let node1 = await daVinci.start()
+ XCTAssertTrue(node1 is SuccessNode)
+ successNode = node1 as! SuccessNode
+
+ user = successNode.user
+ userToken = await user?.token()
+ switch userToken! {
+ case .success(let token):
+ XCTAssertNotNil(token.accessToken)
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ let u = await daVinci.user()
+ await u?.logout() ?? { XCTFail("User is null") }()
+
+ // After logout make sure the user is null
+ let daVinciUser = await daVinci.user()
+ XCTAssertNil(daVinciUser)
+ }
+
+ // TestRailCase(21253)
+ func testUserRegistrationSuccess() async throws {
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ var continueNode = node as! ContinueNode
+
+ // Make sure that we are at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Click the registration link
+ (continueNode.collectors[3] as? FlowCollector)?.value = "register"
+ node = await continueNode.next()
+ XCTAssertTrue(node is ContinueNode)
+ continueNode = node as! ContinueNode
+
+ // Validate the registration form
+ XCTAssertTrue(continueNode.collectors.count == 6)
+ XCTAssertEqual("Registration Form", continueNode.name)
+ XCTAssertEqual("Collect Name, Email, Password", continueNode.description)
+
+ XCTAssertTrue(continueNode.collectors[0] is TextCollector) // Email
+ XCTAssertTrue(continueNode.collectors[1] is PasswordCollector) // Password
+ XCTAssertTrue(continueNode.collectors[2] is TextCollector) // Given Name
+ XCTAssertTrue(continueNode.collectors[3] is TextCollector) // Family Name
+ XCTAssertTrue(continueNode.collectors[4] is SubmitCollector) // Continue
+ XCTAssertTrue(continueNode.collectors[5] is FlowCollector) // Already have an account (link)
+
+ XCTAssertEqual("Email", (continueNode.collectors[0] as! TextCollector).label)
+ XCTAssertEqual("Password", (continueNode.collectors[1] as! PasswordCollector).label)
+ XCTAssertEqual("Given Name", (continueNode.collectors[2] as! TextCollector).label)
+ XCTAssertEqual("Family Name", (continueNode.collectors[3] as! TextCollector).label)
+ XCTAssertEqual("Continue", (continueNode.collectors[4] as! SubmitCollector).label)
+ XCTAssertEqual("Already have an account? Sign On", (continueNode.collectors[5] as! FlowCollector).label)
+
+ // Fill in the registration form
+ let date = Date()
+ let formatter = DateFormatter()
+ formatter.dateFormat = "HHmmssSSSS"
+
+ let newUser = "e2e" + formatter.string(from: date) + "@example.com"
+
+ (continueNode.collectors[0] as? TextCollector)?.value = newUser
+ (continueNode.collectors[1] as? PasswordCollector)?.value = password
+ (continueNode.collectors[2] as? TextCollector)?.value = userFname
+ (continueNode.collectors[3] as? TextCollector)?.value = userLname
+ (continueNode.collectors[4] as? SubmitCollector)?.value = "Save"
+
+ // Click continue
+ (continueNode.collectors[4] as! SubmitCollector).value = "Save"
+
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // User should be navigated to the verification code screen
+ XCTAssertTrue(continueNode.collectors.count == 3 )
+ XCTAssertTrue(continueNode.collectors[0] is TextCollector)
+ XCTAssertTrue(continueNode.collectors[1] is SubmitCollector)
+ XCTAssertTrue(continueNode.collectors[2] is FlowCollector)
+
+ XCTAssertEqual("Enter verification code", continueNode.name)
+ XCTAssertEqual("Hint: The verification code is 1234", continueNode.description)
+ XCTAssertEqual("Verification Code", (continueNode.collectors[0] as! TextCollector).label)
+ XCTAssertEqual("Verify", (continueNode.collectors[1] as! SubmitCollector).label)
+ XCTAssertEqual("Resend Verification Code", (continueNode.collectors[2] as! FlowCollector).label)
+
+ // Fill in the verification code and submit
+ (continueNode.collectors[0] as? TextCollector)?.value = verificationCode
+ (continueNode.collectors[1] as? SubmitCollector)?.value = "Verify"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // User should be navigated to the "Successful user creation" screen...
+ XCTAssertTrue(continueNode.collectors.count == 1 )
+ XCTAssertTrue(continueNode.collectors[0] is SubmitCollector)
+
+ XCTAssertEqual("Registration Complete", continueNode.name)
+ XCTAssertEqual("Notify User Account Is Successfully Created", continueNode.description)
+ XCTAssertEqual("Continue", (continueNode.collectors[0] as! SubmitCollector).label)
+
+ // Click "Continue" to finish the registration process
+ (continueNode.collectors[0] as? SubmitCollector)?.value = "Continue"
+ node = await continueNode.next()
+ XCTAssertTrue(node is SuccessNode)
+ let successNode = node as! SuccessNode
+
+ let user = successNode.user
+ let userToken = await user?.token()
+ switch userToken! {
+ case .success(let token):
+ XCTAssertNotNil(token.accessToken)
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ let u = await daVinci.user()
+ await u?.logout() ?? { XCTFail("User is null") }()
+
+ // After logout make sure the user is null
+ let daVinciUser = await daVinci.user()
+ XCTAssertNil(daVinciUser)
+
+ // Delete the user from PingOne
+ try await deleteUser(userName: newUser, pass: password)
+ }
+
+ // TestRailCase(21269)
+ func testUserRegistrationFailureUserAlreadyExists() async throws {
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ var continueNode = node as! ContinueNode
+
+ // Make sure that we are at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Click the registration link
+ (continueNode.collectors[3] as? FlowCollector)?.value = "register"
+ node = await continueNode.next()
+ XCTAssertTrue(node is ContinueNode)
+ continueNode = node as! ContinueNode
+
+ // Make sure that we are at the registration form
+ XCTAssertEqual("Registration Form", continueNode.name)
+
+ // Fill the registration form with username that already exists
+ (continueNode.collectors[0] as? TextCollector)?.value = username
+ (continueNode.collectors[1] as? PasswordCollector)?.value = password
+ (continueNode.collectors[2] as? TextCollector)?.value = userFname
+ (continueNode.collectors[3] as? TextCollector)?.value = userLname
+ (continueNode.collectors[4] as? SubmitCollector)?.value = "Save"
+
+ // Click continue
+ (continueNode.collectors[4] as! SubmitCollector).value = "Save"
+
+ node = await continueNode.next()
+ let errorNode = node as! ErrorNode
+
+ // Make sure we get the expected error
+ XCTAssertEqual("uniquenessViolation", String(describing: errorNode.input["code"]!))
+ XCTAssertEqual("400", String(describing: errorNode.input["httpResponseCode"]!))
+ XCTAssertEqual("An account with that email address already exists.", errorNode.message)
+
+ // Make sure that we are still at the registration form
+ XCTAssertEqual("Registration Form", continueNode.name)
+ }
+
+ // TestRailCase(21270)
+ func testUserRegistrationFailureInvalidEmail() async throws {
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ var continueNode = node as! ContinueNode
+
+ // Make sure that we are at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Click the registration link
+ (continueNode.collectors[3] as? FlowCollector)?.value = "register"
+ node = await continueNode.next()
+ XCTAssertTrue(node is ContinueNode)
+ continueNode = node as! ContinueNode
+
+ // Make sure that we are at the registration form
+ XCTAssertEqual("Registration Form", continueNode.name)
+
+ // Enter invalid (empty) email in the registration form
+ (continueNode.collectors[0] as? TextCollector)?.value = ""
+ (continueNode.collectors[1] as? PasswordCollector)?.value = password
+ (continueNode.collectors[2] as? TextCollector)?.value = userFname
+ (continueNode.collectors[3] as? TextCollector)?.value = userLname
+ (continueNode.collectors[4] as? SubmitCollector)?.value = "Save"
+
+ // Click continue
+ (continueNode.collectors[4] as! SubmitCollector).value = "Save"
+
+ node = await continueNode.next()
+ let errorNode = node as! ErrorNode
+
+ // Make sure we get the expected error
+ XCTAssertEqual("invalidInput", String(describing: errorNode.input["code"]!))
+ XCTAssertEqual("400", String(describing: errorNode.input["httpResponseCode"]!))
+ XCTAssertEqual("Enter a valid email address", errorNode.message)
+
+ // Make sure that we are still at the registration form
+ XCTAssertEqual("Registration Form", continueNode.name)
+ }
+
+ // TestRailCase(21272)
+ func testUserRegistrationFailureInvalidPassword() async throws {
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ var continueNode = node as! ContinueNode
+
+ // Make sure that we are at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Click the registration link
+ (continueNode.collectors[3] as? FlowCollector)?.value = "register"
+ node = await continueNode.next()
+ XCTAssertTrue(node is ContinueNode)
+ continueNode = node as! ContinueNode
+
+ // Make sure that we are at the registration form
+ XCTAssertEqual("Registration Form", continueNode.name)
+
+ let date = Date()
+ let formatter = DateFormatter()
+ formatter.dateFormat = "HHmmssSSSS"
+
+ let newUser = "e2e" + formatter.string(from: date) + "@example.com"
+
+ // Enter invalid password in the registration form
+ (continueNode.collectors[0] as? TextCollector)?.value = newUser
+ // Note: The password rules for the E2E Tests population require at least one number
+ (continueNode.collectors[1] as? PasswordCollector)?.value = "invalid"
+ (continueNode.collectors[2] as? TextCollector)?.value = userFname
+ (continueNode.collectors[3] as? TextCollector)?.value = userLname
+ (continueNode.collectors[4] as? SubmitCollector)?.value = "Save"
+
+ // Click continue
+ (continueNode.collectors[4] as! SubmitCollector).value = "Save"
+
+ node = await continueNode.next()
+ let errorNode = node as! ErrorNode
+
+ // Make sure we get the expected error
+ XCTAssertEqual("invalidValue", String(describing: errorNode.input["code"]!))
+ XCTAssertEqual("400", String(describing: errorNode.input["httpResponseCode"]!))
+ XCTAssertEqual("password: User password did not satisfy password policy requirements", errorNode.message)
+
+ // Make sure that we are still at the registration form
+ XCTAssertEqual("Registration Form", continueNode.name)
+ }
+
+ // TestRailCase(21273)
+ func testUserRegistrationFailureInvalidVerificationCode() async throws {
+ var node = await daVinci.start()
+ XCTAssertTrue(node is ContinueNode)
+ var continueNode = node as! ContinueNode
+
+ // Make sure that we are at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Click the registration link
+ (continueNode.collectors[3] as? FlowCollector)?.value = "register"
+ node = await continueNode.next()
+ XCTAssertTrue(node is ContinueNode)
+ continueNode = node as! ContinueNode
+
+ // Make sure that we are at the registration form
+ XCTAssertEqual("Registration Form", continueNode.name)
+
+ let date = Date()
+ let formatter = DateFormatter()
+ formatter.dateFormat = "HHmmssSSSS"
+
+ let newUser = "e2e" + formatter.string(from: date) + "@example.com"
+
+ // Fill the registration form
+ (continueNode.collectors[0] as? TextCollector)?.value = newUser
+ (continueNode.collectors[1] as? PasswordCollector)?.value = password
+ (continueNode.collectors[2] as? TextCollector)?.value = userFname
+ (continueNode.collectors[3] as? TextCollector)?.value = userLname
+ (continueNode.collectors[4] as? SubmitCollector)?.value = "Save"
+
+ // Click continue
+ (continueNode.collectors[4] as! SubmitCollector).value = "Save"
+
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // Make sure that we are at the "Verification Code" screen
+ XCTAssertEqual("Enter verification code", continueNode.name)
+
+ // Fill in the verification code and submit
+ (continueNode.collectors[0] as? TextCollector)?.value = "invalid"
+ (continueNode.collectors[1] as? SubmitCollector)?.value = "Verify"
+ node = await continueNode.next()
+ let errorNode = node as! ErrorNode
+
+ // Make sure we get the expected error
+ XCTAssertEqual("400", String(describing: errorNode.input["code"]!))
+ XCTAssertEqual("Invalid verification code", errorNode.message)
+
+ // Make sure that we are still at verification code page
+ XCTAssertEqual("Enter verification code", continueNode.name)
+
+ let daVinciUser = await daVinci.user()
+ XCTAssertNil(daVinciUser)
+ try await deleteUser(userName: newUser, pass: password)
+ }
+
+ // TestRailCase(21277)
+ func testPasswordRecovery() async throws {
+ let date = Date()
+ let formatter = DateFormatter()
+ formatter.dateFormat = "HHmmssSSSS"
+ let newUser = "e2e" + formatter.string(from: date) + "@example.com"
+
+ // Register a test user
+ try await registerUser(userName: newUser, password: password)
+
+ // Launch DaVinci...
+ var node = await daVinci.start()
+ var continueNode = node as! ContinueNode
+
+ // Make sure that we are at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Click on the "Having trouble..." link
+ (continueNode.collectors[4] as? FlowCollector)?.value = "click"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // At the "User Identifier Form" screen...
+ XCTAssertTrue(continueNode.collectors.count == 3)
+ XCTAssertEqual("User Identifier Form", continueNode.name)
+ XCTAssertEqual("Prompt For Email To Send Instructions To Reset Password", continueNode.description)
+
+ XCTAssertTrue(continueNode.collectors[0] is TextCollector) // Username
+ XCTAssertTrue(continueNode.collectors[1] is SubmitCollector) // Continue
+ XCTAssertTrue(continueNode.collectors[2] is FlowCollector) // Back (link)
+
+ XCTAssertEqual("Username", (continueNode.collectors[0] as! TextCollector).label)
+ XCTAssertEqual("Continue", (continueNode.collectors[1] as! SubmitCollector).label)
+ XCTAssertEqual("Back", (continueNode.collectors[2] as! FlowCollector).label)
+
+ // Fill in the username and submit
+ (continueNode.collectors[0] as? TextCollector)?.value = newUser
+ (continueNode.collectors[1] as? SubmitCollector)?.value = "Submit"
+
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // At the "Password Recovery Form" screen...
+ XCTAssertTrue(continueNode.collectors.count == 6)
+ XCTAssertEqual("Password Recovery Form", continueNode.name)
+ XCTAssertEqual("Enter The Recovery Code and Set New Password (Hint: Recovery code is 1234)", continueNode.description)
+
+ XCTAssertTrue(continueNode.collectors[0] is TextCollector) // Recovery Code
+ XCTAssertTrue(continueNode.collectors[1] is PasswordCollector) // New Password
+ XCTAssertTrue(continueNode.collectors[2] is PasswordCollector) // Verify New Password
+ XCTAssertTrue(continueNode.collectors[3] is SubmitCollector) // Continue button
+ XCTAssertTrue(continueNode.collectors[4] is FlowCollector) // Resend recovery code
+ XCTAssertTrue(continueNode.collectors[5] is FlowCollector) // Cancel link
+
+ XCTAssertEqual("Recovery Code", (continueNode.collectors[0] as! TextCollector).label)
+ XCTAssertEqual("New Password", (continueNode.collectors[1] as! PasswordCollector).label)
+ XCTAssertEqual("Verify New Password", (continueNode.collectors[2] as! PasswordCollector).label)
+ XCTAssertEqual("Continue", (continueNode.collectors[3] as! SubmitCollector).label)
+ XCTAssertEqual("Resend recovery code", (continueNode.collectors[4] as! FlowCollector).label)
+ XCTAssertEqual("Cancel", (continueNode.collectors[5] as! FlowCollector).label)
+
+ // Fill in the recovery code and new password and submit
+ (continueNode.collectors[0] as? TextCollector)?.value = verificationCode
+ (continueNode.collectors[1] as? PasswordCollector)?.value = newPassword
+ (continueNode.collectors[2] as? PasswordCollector)?.value = newPassword
+ (continueNode.collectors[3] as? SubmitCollector)?.value = "Submit"
+
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // At the "Successful password reset" screen...
+ XCTAssertTrue(continueNode.collectors.count == 1)
+ XCTAssertEqual("Password Reset Success", continueNode.name)
+ XCTAssertEqual("Success Message With Animated Checkmark", continueNode.description)
+ XCTAssertEqual("Continue", (continueNode.collectors[0] as! SubmitCollector).label)
+
+ // Click "Continue" to finish the password reset process
+ (continueNode.collectors[0] as? SubmitCollector)?.value = "Continue"
+
+ node = await continueNode.next()
+ let successNode = node as! SuccessNode
+
+ let user = successNode.user
+ let userToken = await user?.token()
+ switch userToken! {
+ case .success(let token):
+ XCTAssertNotNil(token.accessToken)
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ let u = await daVinci.user()
+ await u?.logout() ?? { XCTFail("User is null") }()
+
+ // After logout make sure the user is null
+ let daVinciUser = await daVinci.user()
+ XCTAssertNil(daVinciUser)
+
+ // Delete the user from PingOne
+ try await deleteUser(userName: newUser, pass: newPassword)
+ }
+
+ // TestRailCase(21278)
+ func testPasswordReset() async throws {
+ let date = Date()
+ let formatter = DateFormatter()
+ formatter.dateFormat = "HHmmssSSSS"
+ let newUser = "e2e" + formatter.string(from: date) + "@example.com"
+
+ // Register a test user
+ try await registerUser(userName: newUser, password: password)
+
+ // Launch DaVinci...
+ var node = await daVinci.start()
+ var continueNode = node as! ContinueNode
+
+ // Make sure that we are at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Login
+ (continueNode.collectors[0] as? TextCollector)?.value = newUser
+ (continueNode.collectors[1] as? PasswordCollector)?.value = password
+ (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // At the "Successful Login" page
+ XCTAssertEqual("Successful login", continueNode.name)
+
+ // Click the "Reset password" link
+ (continueNode.collectors[1] as? FlowCollector)?.value = "click"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // At the "Change Password Form" screen...
+ XCTAssertTrue(continueNode.collectors.count == 5)
+ XCTAssertEqual("Change Password Form", continueNode.name)
+ XCTAssertEqual("Prompt for existing and new password", continueNode.description)
+
+ XCTAssertTrue(continueNode.collectors[0] is PasswordCollector) // Current Password
+ XCTAssertTrue(continueNode.collectors[1] is PasswordCollector) // New Password
+ XCTAssertTrue(continueNode.collectors[2] is PasswordCollector) // Verify New Password
+ XCTAssertTrue(continueNode.collectors[3] is SubmitCollector) // Continue button
+ XCTAssertTrue(continueNode.collectors[4] is FlowCollector) // Cancel (link)
+
+ XCTAssertEqual("Current Password", (continueNode.collectors[0] as! PasswordCollector).label)
+ XCTAssertEqual("New Password", (continueNode.collectors[1] as! PasswordCollector).label)
+ XCTAssertEqual("Verify New Password", (continueNode.collectors[2] as! PasswordCollector).label)
+ XCTAssertEqual("Continue", (continueNode.collectors[3] as! SubmitCollector).label)
+ XCTAssertEqual("Cancel", (continueNode.collectors[4] as! FlowCollector).label)
+
+ // Fill in the reset password form and submit
+ (continueNode.collectors[0] as? PasswordCollector)?.value = password
+ (continueNode.collectors[1] as? PasswordCollector)?.value = newPassword
+ (continueNode.collectors[2] as? PasswordCollector)?.value = newPassword
+ (continueNode.collectors[3] as? SubmitCollector)?.value = "Submit"
+
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // At the "Password Reset Success" screen...
+ XCTAssertTrue(continueNode.collectors.count == 1)
+ XCTAssertEqual("Password Reset Success", continueNode.name)
+ XCTAssertEqual("Success Message With Animated Checkmark", continueNode.description)
+ XCTAssertTrue(continueNode.collectors[0] is SubmitCollector) // Continue button
+
+ // Click "Continue" to finish the password reset process
+ (continueNode.collectors[0] as? SubmitCollector)?.value = "Continue"
+
+ node = await continueNode.next()
+ let successNode = node as! SuccessNode
+
+ let user = successNode.user
+ let userToken = await user?.token()
+ switch userToken! {
+ case .success(let token):
+ XCTAssertNotNil(token.accessToken)
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ let u = await daVinci.user()
+ await u?.logout() ?? { XCTFail("User is null") }()
+
+ // After logout make sure the user is null
+ let daVinciUser = await daVinci.user()
+ XCTAssertNil(daVinciUser)
+
+ // Delete the user from PingOne
+ try await deleteUser(userName: newUser, pass: newPassword)
+ }
+
+ // TestRailCase(24629)
+ func testAccountLocked() async throws {
+ let date = Date()
+ let formatter = DateFormatter()
+ formatter.dateFormat = "HHmmssSSSS"
+ let newUser = "e2e" + formatter.string(from: date) + "@example.com"
+
+ // Register a test user
+ try await registerUser(userName: newUser, password: password)
+
+ // Launch DaVinci...
+ var node = await daVinci.start()
+ var continueNode = node as! ContinueNode
+
+ // Make sure that we are at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Fill in the login form with invalid credentials and submit...
+ // The following rules apply for the E2E test population:
+ // Account Lockout Rules:
+ // The user's account will be locked out after 2 distinct failed password attempts;
+ // repeated attempts of the same password are not counted.
+ // Automatically unlock accounts that were locked by failed password attempts after 1 second
+ (continueNode.collectors[0] as? TextCollector)?.value = newUser
+ (continueNode.collectors[1] as? PasswordCollector)?.value = "wrong1"
+ (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On"
+ node = await continueNode.next()
+ let errorNode = node as! ErrorNode
+
+ // Make sure we get the expected error
+ XCTAssertEqual("Invalid username and/or password", errorNode.message)
+
+ // Make sure that we are still at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Enter invalid credentials again (this should lock the account)
+ (continueNode.collectors[0] as? TextCollector)?.value = newUser
+ (continueNode.collectors[1] as? PasswordCollector)?.value = "wrong2"
+ (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // Make sure that we are at the "Account Locked" screen...
+ XCTAssertTrue(continueNode.collectors.count == 1)
+ XCTAssertEqual("Account Locked Message", continueNode.name)
+ XCTAssertEqual("Notify when account will unlock", continueNode.description)
+ XCTAssertTrue(continueNode.collectors[0] is FlowCollector) // Back to sign on (link)
+ XCTAssertEqual("Back to sign on", (continueNode.collectors[0] as! FlowCollector).label)
+
+ // Click "back" to return to the login page
+ (continueNode.collectors[0] as? SubmitCollector)?.value = "Back"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // Ensure that we are now at the login page and user is not logged in
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+ var daVinciUser = await daVinci.user()
+ XCTAssertNil(daVinciUser)
+
+ // Wait for a second so that the account gets unlocked
+ try await Task.sleep(nanoseconds: 1 * 1_500_000_000)
+
+ // Enter the valid username and password of the account that was locked
+ (continueNode.collectors[0] as? TextCollector)?.value = newUser
+ (continueNode.collectors[1] as? PasswordCollector)?.value = password
+ (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On" // This will submit the form...
+ node = await continueNode.next()
+ XCTAssertTrue(node is ContinueNode)
+ continueNode = node as! ContinueNode
+
+ // Verify that the login was successful
+ XCTAssertEqual("Successful login", continueNode.name)
+
+ // Click continue
+ (continueNode.collectors[0] as! SubmitCollector).value = "Continue"
+ node = await continueNode.next()
+ XCTAssertTrue(node is SuccessNode)
+ let successNode = node as! SuccessNode
+
+ let user = successNode.user
+ let userToken = await user?.token()
+ switch userToken! {
+ case .success(let token):
+ XCTAssertNotNil(token.accessToken)
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ let u = await daVinci.user()
+ await u?.logout() ?? { XCTFail("User is null") }()
+
+ // After logout make sure the user is null
+ daVinciUser = await daVinci.user()
+ XCTAssertNil(daVinciUser)
+
+ // Delete the test user
+ try await deleteUser(userName: newUser, pass: password)
+
+ }
+
+ /// Helper function to register a user
+ private func registerUser(userName: String, password: String) async throws {
+ var node = await daVinci.start()
+ var continueNode = node as! ContinueNode
+
+ // Make sure that we are at the login form
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ // Click the registration link
+ (continueNode.collectors[3] as? FlowCollector)?.value = "register"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // Make sure that we are at the registration form
+ XCTAssertEqual("Registration Form", continueNode.name)
+
+ // Fill in the registration form
+ (continueNode.collectors[0] as? TextCollector)?.value = userName
+ (continueNode.collectors[1] as? PasswordCollector)?.value = password
+ (continueNode.collectors[2] as? TextCollector)?.value = userFname
+ (continueNode.collectors[3] as? TextCollector)?.value = userLname
+ (continueNode.collectors[4] as? SubmitCollector)?.value = "Save"
+
+ // Click continue
+ (continueNode.collectors[4] as! SubmitCollector).value = "Save"
+
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // User should be navigated to the verification code screen
+ XCTAssertEqual("Enter verification code", continueNode.name)
+
+ // Fill in the verification code and submit
+ (continueNode.collectors[0] as? TextCollector)?.value = verificationCode
+ (continueNode.collectors[1] as? SubmitCollector)?.value = "Verify"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // User should be navigated to the "Successful user creation" screen...
+ XCTAssertEqual("Registration Complete", continueNode.name)
+
+ // Click "Continue" to finish the registration process
+ (continueNode.collectors[0] as? SubmitCollector)?.value = "Continue"
+ node = await continueNode.next()
+ XCTAssertTrue(node is SuccessNode)
+
+ // logout the user
+ let u = await daVinci.user()
+ await u?.logout() ?? { XCTFail("User is null") }()
+ }
+
+ /// Helper function to delete a user
+ private func deleteUser(userName: String, pass: String) async throws {
+ var node = await daVinci.start()
+ var continueNode = node as! ContinueNode
+
+ // // Make sure that we are at the Login form...
+ XCTAssertEqual("E2E Login Form", continueNode.name)
+
+ (continueNode.collectors[0] as? TextCollector)?.value = userName
+ (continueNode.collectors[1] as? PasswordCollector)?.value = pass
+ (continueNode.collectors[2] as? SubmitCollector)?.value = "Sign On"
+ node = await continueNode.next()
+ XCTAssertTrue(node is ContinueNode)
+ continueNode = node as! ContinueNode
+
+ // Verify the Successful login form
+ XCTAssertEqual("Successful login", continueNode.name)
+ XCTAssertEqual("Successfully logged in to DaVinci", continueNode.description)
+ XCTAssertEqual("Continue", (continueNode.collectors[0] as! SubmitCollector).label)
+ XCTAssertEqual("Reset password...", (continueNode.collectors[1] as! FlowCollector).label)
+ XCTAssertEqual("Delete user...", (continueNode.collectors[2] as! FlowCollector).label)
+
+ // Click the "Delete user" link
+ (continueNode.collectors[2] as? FlowCollector)?.value = "Delete User"
+ node = await continueNode.next()
+ continueNode = node as! ContinueNode
+
+ // Validate success user deletion screen
+ XCTAssertEqual("Success", continueNode.name)
+ XCTAssertEqual("User has been successfully deleted", continueNode.description)
+ }
+}
diff --git a/Davinci/DavinciTests/mock/MockAPIEndpoint.swift b/Davinci/DavinciTests/mock/MockAPIEndpoint.swift
new file mode 100644
index 0000000..ffc3f93
--- /dev/null
+++ b/Davinci/DavinciTests/mock/MockAPIEndpoint.swift
@@ -0,0 +1,43 @@
+//
+// MockAPIEndpoint.swift
+// DavinciTests
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+enum MockAPIEndpoint {
+ static let baseURL = "https://auth.test-one-pingone.com"
+
+ case authorization
+ case token
+ case userinfo
+ case endSession
+ case revocation
+ case discovery
+ case customHTMLTemplate
+
+ var url: URL {
+ switch self {
+ case .authorization:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/authorize")!
+ case .token:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/token")!
+ case .userinfo:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/userinfo")!
+ case .endSession:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/signoff")!
+ case .revocation:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/revoke")!
+ case .discovery:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/.well-known/openid-configuration")!
+ case .customHTMLTemplate:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/customHTMLTemplate")!
+ }
+ }
+}
diff --git a/Davinci/DavinciTests/mock/MockResponse.swift b/Davinci/DavinciTests/mock/MockResponse.swift
new file mode 100644
index 0000000..246efd1
--- /dev/null
+++ b/Davinci/DavinciTests/mock/MockResponse.swift
@@ -0,0 +1,222 @@
+//
+// MockResponse.swift
+// DavinciTests
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+struct MockResponse {
+ static let headers = ["Content-Type": "application/json"]
+
+ // Freturn the OpenID configuration response as Data
+ static var openIdConfigurationResponse: Data {
+ return """
+ {
+ "authorization_endpoint" : "http://auth.test-one-pingone.com/authorize",
+ "token_endpoint" : "https://auth.test-one-pingone.com/token",
+ "userinfo_endpoint" : "https://auth.test-one-pingone.com/userinfo",
+ "end_session_endpoint" : "https://auth.test-one-pingone.com/signoff",
+ "revocation_endpoint" : "https://auth.test-one-pingone.com/revoke"
+ }
+ """.data(using: .utf8)!
+ }
+
+ // return the token response as Data
+ static var tokenResponse: Data {
+ return """
+ {
+ "access_token" : "Dummy AccessToken",
+ "token_type" : "Dummy Token Type",
+ "scope" : "openid email address",
+ "refresh_token" : "Dummy RefreshToken",
+ "expires_in" : 1,
+ "id_token" : "Dummy IdToken"
+ }
+ """.data(using: .utf8)!
+ }
+
+ // return the userinfo response as Data
+ static var userinfoResponse: Data {
+ return """
+ {
+ "sub" : "test-sub",
+ "name" : "test-name",
+ "email" : "test-email",
+ "phone_number" : "test-phone_number",
+ "address" : "test-address"
+ }
+ """.data(using: .utf8)!
+ }
+
+ // return an empty revoke response as Data
+ static var revokeResponse: Data{
+ return Data()
+ }
+
+ // Headers for the authorize response
+ static let authorizeResponseHeaders: [String: String] =
+ [
+ "Content-Type": "application/json; charset=utf-8",
+ "Set-Cookie": """
+ interactionId=038e8128-272a-4a15-b97b-379aa1447149; Max-Age=3600; Path=/; Expires=Wed, 27 Mar 9999 05:06:30 GMT; HttpOnly
+ """,
+// "Set-Cookie": """
+// interactionToken=71c65504463355679fd247900441c36afb6be6c00d45aa169500b7cd753894d46d68feb4952ff0843ff4b287220a66cb3d58a3bc41e71724f111b034d0458aac8a5153859ed96825ef8c6a6400e7ae9de82a7353fc3c9886ba835853db8c0957ea4cd0a52d20d4fb50b4419dc9df33a53889f52abeb04f517b6c7c8efb0b58f0; Max-Age=3600; Path=/; Expires=Wed, 27 Mar 9999 05:06:30 GMT; HttpOnly
+// """,
+// "Set-Cookie": """
+// skProxyApiEnvironmentId=us-west-2; Max-Age=900; Path=/; Expires=Wed, 27 Mar 9999 04:21:30 GMT; HttpOnly
+// """
+ ]
+
+ // return the authorize response as Data
+ static var authorizeResponse: Data {
+ return """
+ {
+ "_links": {
+ "next": {
+ "href": "http://auth.test-one-pingone.com/customHTMLTemplate"
+ }
+ },
+ "interactionId": "008bccea-914b-49da-b2a1-5cd3f83f4372",
+ "interactionToken": "2a0d9bcdbdeb5ea14ef34d680afc45f37a56e190e306a778f01d768b271bf1e976aaf4154b633381e1299b684d3a4a66d3e1c6d419a7d20657bf4f32c741d78f67d41e08eb0e5f1070edf780809b4ccea8830866bcedb388d8f5de13e89454d353bcca86d4dcd5d7872efc929f7e5199d8d127d1b2b45499c42856ce785d8664",
+ "eventName": "continue",
+ "isResponseCompatibleWithMobileAndWebSdks": true,
+ "id": "cq77vwelou",
+ "companyId": "0c6851ed-0f12-4c9a-a174-9b1bf8b438ae",
+ "flowId": "ebac77c8fbf68d3dac68c5dd804a936f",
+ "connectionId": "867ed4363b2bc21c860085ad2baa817d",
+ "capabilityName": "customHTMLTemplate",
+ "formData": {
+ "value": {
+ "username": "",
+ "password": ""
+ }
+ },
+ "form": {
+ "name": "Username/Password Form",
+ "description": "Test Description",
+ "category": "CUSTOM_HTML",
+ "components": {
+ "fields": [
+ {
+ "type": "TEXT",
+ "key": "username",
+ "label": "Username"
+ },
+ {
+ "type": "PASSWORD",
+ "key": "password",
+ "label": "Password"
+ },
+ {
+ "type": "SUBMIT_BUTTON",
+ "key": "SIGNON",
+ "label": "Sign On"
+ },
+ {
+ "type": "FLOW_BUTTON",
+ "key": "TROUBLE",
+ "label": "Having trouble signing on?"
+ },
+ {
+ "type": "FLOW_BUTTON",
+ "key": "REGISTER",
+ "label": "No account? Register now!"
+ }
+ ]
+ }
+ }
+ }
+ """.data(using: .utf8)!
+ }
+
+ // Headers for the custom HTML template response
+ static let customHTMLTemplateHeaders: [String: String] = [
+ "Content-Type": "application/json; charset=utf-8",
+ "Set-Cookie": """
+ ST=session_token; Max-Age=3600; Path=/; Expires=Wed, 27 Mar 9999 05:06:30 GMT; HttpOnly
+ """
+ ]
+
+ // return the custom HTML template response as Data
+ static var customHTMLTemplate: Data {
+ return """
+ {
+ "interactionId": "033e1338-c271-4dd7-8d74-fc2eacc135d8",
+ "companyId": "94e3268d-847d-47aa-a45e-1ef8dd8f4df0",
+ "connectionId": "26146c8065741406afb0899484e361a7",
+ "connectorId": "pingOneAuthenticationConnector",
+ "id": "5dtrjnrwox",
+ "capabilityName": "returnSuccessResponseRedirect",
+ "environment": {
+ "id": "94e3268d-847d-47aa-a45e-1ef8dd8f4df0"
+ },
+ "session": {
+ "id": "d0598645-c2f7-4b94-adc9-401a896eaffb"
+ },
+ "status": "COMPLETED",
+ "authorizeResponse": {
+ "code": "03dbd5a2-db72-437c-8728-fc33b860083c"
+ },
+ "success": true,
+ "interactionToken": "5ad09feac8982d668c5f07d1eaf544bdf2309247146999c0139f7ebb955c24743b97a01e3bf67360121cd85d7a9e1d966c3f4b7e27f21206a5304d305951864cc34a37900f3326f8000c7bc731af9ba78a681eb14d4bf767172e8a7149e4df3e054b4245bdea5612e9ec0c0d8cb349b55dcf10db30de075dfc79f6c765046d99"
+ }
+ """.data(using: .utf8)!
+ }
+
+ // return the custom HTML template response with invalid password as Data
+ static var customHTMLTemplateWithInvalidPassword: Data {
+ return """
+ {
+ "interactionId": "00444ecd-0901-4b57-acc3-e1245971205b",
+ "companyId": "0c6851ed-0f12-4c9a-a174-9b1bf8b438ae",
+ "connectionId": "94141bf2f1b9b59a5f5365ff135e02bb",
+ "connectorId": "pingOneSSOConnector",
+ "id": "dnu7jt3sjz",
+ "capabilityName": "checkPassword",
+ "errorCategory": "NotSet",
+ "code": "Invalid username and/or password",
+ "cause": null,
+ "expected": true,
+ "message": "Invalid username and/or password",
+ "httpResponseCode": 400,
+ "details": [
+ {
+ "rawResponse": {
+ "id": "b187c1c7-e9fe-4f72-a554-1b2876babafe",
+ "code": "INVALID_DATA",
+ "message": "The request could not be completed. One or more validation errors were in the request.",
+ "details": [
+ {
+ "code": "INVALID_VALUE",
+ "target": "password",
+ "message": "The provided password did not match provisioned password",
+ "innerError": {
+ "failuresRemaining": 4
+ }
+ }
+ ]
+ },
+ "statusCode": 400
+ }
+ ],
+ "isResponseCompatibleWithMobileAndWebSdks": true,
+ "correlationId": "b187c1c7-e9fe-4f72-a554-1b2876babafe"
+ }
+ """.data(using: .utf8)!
+ }
+
+ static var tokenErrorResponse: Data {
+ return """
+ {
+ "error": "Invalid Grant"
+ }
+ """.data(using: .utf8)!
+ }
+}
diff --git a/Davinci/DavinciTests/mock/MockURLProtocol.swift b/Davinci/DavinciTests/mock/MockURLProtocol.swift
new file mode 100644
index 0000000..a62720a
--- /dev/null
+++ b/Davinci/DavinciTests/mock/MockURLProtocol.swift
@@ -0,0 +1,57 @@
+//
+// MockURLProtocol.swift
+// DavinciTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+
+class MockURLProtocol: URLProtocol {
+ public static var requestHistory: [URLRequest] = [URLRequest]()
+
+ static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
+
+ static func startInterceptingRequests() {
+ URLProtocol.registerClass(MockURLProtocol.self)
+ }
+
+ static func stopInterceptingRequests() {
+ URLProtocol.unregisterClass(MockURLProtocol.self)
+ requestHistory.removeAll()
+ }
+
+ override class func canInit(with request: URLRequest) -> Bool {
+ return true
+ }
+
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+ return request
+ }
+
+ override func startLoading() {
+ MockURLProtocol.requestHistory.append(request)
+
+ guard let handler = MockURLProtocol.requestHandler else {
+ XCTFail("Received unexpected request with no handler set")
+ return
+ }
+ do {
+ let (response, data) = try handler(request)
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ client?.urlProtocol(self, didLoad: data)
+ client?.urlProtocolDidFinishLoading(self)
+ } catch {
+ client?.urlProtocol(self, didFailWithError: error)
+ }
+ }
+
+ override func stopLoading() {
+
+ }
+}
diff --git a/Davinci/README.md b/Davinci/README.md
new file mode 100644
index 0000000..546cfeb
--- /dev/null
+++ b/Davinci/README.md
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+# PingDavinci
+
+## Overview
+
+PingDavinci is a powerful and flexible library for Authentication and Authorization. It is designed to be easy to use and
+extensible. It provides a simple API for navigating the authentication flow and handling the various states that can
+occur during the authentication process.
+
+
+
+## Integrating the SDK into your project
+
+Use Cocoapods or Swift Package Manger
+
+## Usage
+
+To use the `DaVinci` class, you need to create an instance of it by passing a configuration block to the `createDaVinci` method. The
+configuration block allows you to customize various aspects of the `DaVinci` instance, such as the timeout and logging.
+
+Here's an example of how to create a `DaVinci` instance:
+
+```swift
+let daVinci = DaVinci.createDaVinci { config in
+ // Oidc as module
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "test"
+ oidcValue.discoveryEndpoint = "https://auth.test-one-pingone.com/0c6851ed-0f12-4c9a-a174-9b1bf8b438ae/as/.well-known/openid-configuration"
+ oidcValue.scopes = ["openid", "email", "address"]
+ oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect"
+ }
+ }
+var node = await daVinci.start()
+node = await (node as! ContinueNode).next()
+```
+
+The `PingDavinci` depends on `PingOidc` module. It discovers the OIDC endpoints with `discoveryEndpoint` attribute.
+
+The `start` method returns a `Node` instance. The `Node` class represents the current state of the application. You can
+use the `next` method to transition to the next state.
+
+## More DaVinci Configuration
+```swift
+let daVinci = DaVinci.createDaVinci { config in
+ config.timeout = 30
+ config.logger = LogManager.standard
+ config.module(OidcModule.config) { oidcValue in
+ //...
+ oidcValue.storage = MemoryStorage()
+ }
+}
+```
+
+
+### Navigate the authentication Flow
+
+```swift
+let node = await daVinci.start() //Start the flow
+
+//Determine the Node Type
+switch (node) {
+case is ContinueNode: do {}
+case is ErrorNode: do {}
+case is FailureNode: do {}
+case is SuccessNode: do {}
+ }
+```
+
+| Node Type | Description |
+|------------|:----------------------------------------------------------------------------------------------------------|
+| ContinueNode | In the middle of the flow, call ```node.next``` to move to next Node in the flow |
+| FailureNode | Unexpected Error, e.g Network, parsing ```node.cause``` to retrieve the cause of the error |
+| ErrorNode| Bad Request from the server, e.g Invalid Password, OTP, username ```node.message``` for the error message |
+| SuccessNode| Authentication successful ```node.session``` to retrieve the session |
+
+### Provide input
+For `ContinueNode` Node, you can access list of Collector with `node.collectors` and provide input to
+the `Collector`.
+Currently, there are, `TextCollector`, `PasswordCollector`, `SubmitCollector`, `FlowCollector`, but more will be added in the future, such as `Fido`,
+`SocialLoginCollector`, etc...
+
+To access the collectors, you can use the following code:
+```swift
+node.collectors.forEach { item in
+ switch(item) {
+ case is TextCollector:
+ (item as! TextCollector).value = "My First Name"
+ case is PasswordCollector:
+ (item as! PasswordCollector).value = "My Password"
+ case is SubmitCollector:
+ (item as! SubmitCollector).value = "click me"
+ case is FlowCollector:
+ (item as! FlowCollector).value = "Forgot Password"
+ }
+}
+
+//Move to next Node, and repeat the flow until it reaches `SuccessNode` or `ErrorNode` Node
+let next = node.next()
+```
+
+### Error Handling
+
+For `FailureNode` Node, you can retrieve the cause of the error by using `node.cause`. The `cause` is an `Error` instance,
+when receiving an error, you cannot continue the Flow, you may want to display a generic message to the user, and report
+the issue to the Support team.
+The Error may include Network issue, parsing issue, API Error (Server response other that 2xx and 400) and other unexpected issues.
+
+For `ErrorNode` Node, you can retrieve the error message by using `node.message`. The `message` is a `String` object,
+when receiving a failure, you can continue the Flow with previous `ContinueNode` Node, but you may want to display the error message to the user.
+e.g "Username/Password is incorrect", "OTP is invalid", etc...
+```swift
+let node = await daVinci.start() //Start the flow
+
+//Determine the Node Type
+switch (node) {
+case is ContinueNode: do {}
+case is FailureNode:
+ (node as! FailureNode).cause //Retrieve the cause of the Failure
+case is ErrorNode:
+ (node as! ErrorNode).message //Retrieve the error message
+case is SuccessNode: do {}
+}
+```
+
+### Node Identifier
+You can use the `node.id` to identify the current state of the flow. The `id` is a unique identifier for each node.
+
+For example, you can use the `id` to determine if the current state is `Forgot Passowrd`, `Registration`, etc....
+
+```swift
+
+var state = ""
+switch (node.id) {
+case "cq77vwelou": state = "Sign On"
+case "qwnvng32z3": state = "Password Reset"
+case "4dth5sn269": state = "Create Your Profile"
+case "qojn9nsdxh": state = "Verification Code"
+case "fkekf3oi8e": state = "Enter New Password"
+default: state = ""
+}
+```
+
+Other than `id`, you can also use `node.name` to retrieve the name of the Node, `node.description` to retrieve the description of the Node.
+
+
+### Work with SwiftUI
+
+ViewModel
+```swift
+//Define State that listen by the View
+
+@Published var state: Node = EmptyNode()
+
+//Start the DaVinci flow
+let next = await daVinci.start()
+
+//Update the state
+state = next
+
+func next(node: ContinueNode) {
+ val next = await node.next()
+ state = next
+
+}
+```
+
+View
+```swift
+if let node = state.node {
+ switch node {
+ case is ContinueNode:
+ // Handle ContinueNode case
+ break
+ case is ErrorNode:
+ // Handle Error case
+ break
+ case is FailureNode:
+ // Handle Failure case
+ break
+ case is SuccessNode:
+ // Handle Success case
+ break
+ default:
+ break
+ }
+}
+```
+
+### Post Authentication
+After authenticate with DaVinci, the user session will be stored in the storage.
+To retrieve the existing session, you can use the following code:
+
+```swift
+//Retrieve the existing user, if token exists in the storage, ```user``` will be not nil.
+//However, even with the user object, you may not be able to retrieve a valid token, as the token and refresh token may be expired.
+
+let user: User? = await daVinci.user()
+
+_ = await user?.token()
+await user?.revoke()
+_ = await user?.userinfo(cache: false)
+await user?.logout()
+
+```
diff --git a/Davinci/images/davinciSequence.png b/Davinci/images/davinciSequence.png
new file mode 100644
index 0000000..916cad4
Binary files /dev/null and b/Davinci/images/davinciSequence.png differ
diff --git a/LICENSE b/LICENSE
index 2e96d7b..dd424e1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2024 ForgeRock Community
+Copyright (c) 2024 Ping Idenitty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Logger/Logger.xcodeproj/project.pbxproj b/Logger/Logger.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..e7e0a8e
--- /dev/null
+++ b/Logger/Logger.xcodeproj/project.pbxproj
@@ -0,0 +1,500 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ A5A712402CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A7123F2CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy */; };
+ A5A796B72BE04E68004D0F2D /* PingLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5A796AE2BE04E67004D0F2D /* PingLogger.framework */; };
+ A5A796BC2BE04E68004D0F2D /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A796BB2BE04E68004D0F2D /* LoggerTests.swift */; };
+ A5A796BD2BE04E68004D0F2D /* Logger.h in Headers */ = {isa = PBXBuildFile; fileRef = A5A796B12BE04E67004D0F2D /* Logger.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ A5A796D02BE04EDE004D0F2D /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A796CF2BE04EDE004D0F2D /* Logger.swift */; };
+ A5A796D22BE050DB004D0F2D /* NoneLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A796D12BE050DB004D0F2D /* NoneLogger.swift */; };
+ A5A796D62BE05C9B004D0F2D /* StandardLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A796D52BE05C9B004D0F2D /* StandardLogger.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ A5A796B82BE04E68004D0F2D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = A5A796A52BE04E67004D0F2D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = A5A796AD2BE04E67004D0F2D;
+ remoteInfo = PingLogger;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ A5A7123F2CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
+ A5A796AE2BE04E67004D0F2D /* PingLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A5A796B12BE04E67004D0F2D /* Logger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Logger.h; sourceTree = ""; };
+ A5A796B62BE04E68004D0F2D /* LoggerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoggerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ A5A796BB2BE04E68004D0F2D /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; };
+ A5A796CF2BE04EDE004D0F2D /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; };
+ A5A796D12BE050DB004D0F2D /* NoneLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoneLogger.swift; sourceTree = ""; };
+ A5A796D52BE05C9B004D0F2D /* StandardLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardLogger.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ A5A796AB2BE04E67004D0F2D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A5A796B32BE04E68004D0F2D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A796B72BE04E68004D0F2D /* PingLogger.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ A5A796A42BE04E67004D0F2D = {
+ isa = PBXGroup;
+ children = (
+ A5A796B02BE04E67004D0F2D /* Logger */,
+ A5A796BA2BE04E68004D0F2D /* LoggerTests */,
+ A5A796AF2BE04E67004D0F2D /* Products */,
+ );
+ sourceTree = "";
+ };
+ A5A796AF2BE04E67004D0F2D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A5A796AE2BE04E67004D0F2D /* PingLogger.framework */,
+ A5A796B62BE04E68004D0F2D /* LoggerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ A5A796B02BE04E67004D0F2D /* Logger */ = {
+ isa = PBXGroup;
+ children = (
+ A5A7123F2CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy */,
+ A5A796B12BE04E67004D0F2D /* Logger.h */,
+ A5A796CF2BE04EDE004D0F2D /* Logger.swift */,
+ A5A796D12BE050DB004D0F2D /* NoneLogger.swift */,
+ A5A796D52BE05C9B004D0F2D /* StandardLogger.swift */,
+ );
+ path = Logger;
+ sourceTree = "";
+ };
+ A5A796BA2BE04E68004D0F2D /* LoggerTests */ = {
+ isa = PBXGroup;
+ children = (
+ A5A796BB2BE04E68004D0F2D /* LoggerTests.swift */,
+ );
+ path = LoggerTests;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+ A5A796A92BE04E67004D0F2D /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A796BD2BE04E68004D0F2D /* Logger.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+ A5A796AD2BE04E67004D0F2D /* PingLogger */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = A5A796C02BE04E68004D0F2D /* Build configuration list for PBXNativeTarget "PingLogger" */;
+ buildPhases = (
+ A5A796A92BE04E67004D0F2D /* Headers */,
+ A5A796AA2BE04E67004D0F2D /* Sources */,
+ A5A796AB2BE04E67004D0F2D /* Frameworks */,
+ A5A796AC2BE04E67004D0F2D /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PingLogger;
+ productName = PingLogger;
+ productReference = A5A796AE2BE04E67004D0F2D /* PingLogger.framework */;
+ productType = "com.apple.product-type.framework";
+ };
+ A5A796B52BE04E68004D0F2D /* LoggerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = A5A796C32BE04E68004D0F2D /* Build configuration list for PBXNativeTarget "LoggerTests" */;
+ buildPhases = (
+ A5A796B22BE04E68004D0F2D /* Sources */,
+ A5A796B32BE04E68004D0F2D /* Frameworks */,
+ A5A796B42BE04E68004D0F2D /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ A5A796B92BE04E68004D0F2D /* PBXTargetDependency */,
+ );
+ name = LoggerTests;
+ productName = PingLoggerTests;
+ productReference = A5A796B62BE04E68004D0F2D /* LoggerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ A5A796A52BE04E67004D0F2D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1530;
+ LastUpgradeCheck = 1530;
+ TargetAttributes = {
+ A5A796AD2BE04E67004D0F2D = {
+ CreatedOnToolsVersion = 15.3;
+ LastSwiftMigration = 1530;
+ };
+ A5A796B52BE04E68004D0F2D = {
+ CreatedOnToolsVersion = 15.3;
+ };
+ };
+ };
+ buildConfigurationList = A5A796A82BE04E67004D0F2D /* Build configuration list for PBXProject "Logger" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = A5A796A42BE04E67004D0F2D;
+ productRefGroup = A5A796AF2BE04E67004D0F2D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ A5A796AD2BE04E67004D0F2D /* PingLogger */,
+ A5A796B52BE04E68004D0F2D /* LoggerTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ A5A796AC2BE04E67004D0F2D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A712402CAC51BE00B7DD58 /* PrivacyInfo.xcprivacy in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A5A796B42BE04E68004D0F2D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ A5A796AA2BE04E67004D0F2D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A796D62BE05C9B004D0F2D /* StandardLogger.swift in Sources */,
+ A5A796D02BE04EDE004D0F2D /* Logger.swift in Sources */,
+ A5A796D22BE050DB004D0F2D /* NoneLogger.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A5A796B22BE04E68004D0F2D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A796BC2BE04E68004D0F2D /* LoggerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ A5A796B92BE04E68004D0F2D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = A5A796AD2BE04E67004D0F2D /* PingLogger */;
+ targetProxy = A5A796B82BE04E68004D0F2D /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ A5A796BE2BE04E68004D0F2D /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ 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";
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Debug;
+ };
+ A5A796BF2BE04E68004D0F2D /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Release;
+ };
+ A5A796C12BE04E68004D0F2D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Logger;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_INSTALL_OBJC_HEADER = NO;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ A5A796C22BE04E68004D0F2D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Logger;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_INSTALL_OBJC_HEADER = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ A5A796C42BE04E68004D0F2D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ OTHER_SWIFT_FLAGS = "";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.LoggerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Debug;
+ };
+ A5A796C52BE04E68004D0F2D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ OTHER_SWIFT_FLAGS = "";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.LoggerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ A5A796A82BE04E67004D0F2D /* Build configuration list for PBXProject "Logger" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A5A796BE2BE04E68004D0F2D /* Debug */,
+ A5A796BF2BE04E68004D0F2D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ A5A796C02BE04E68004D0F2D /* Build configuration list for PBXNativeTarget "PingLogger" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A5A796C12BE04E68004D0F2D /* Debug */,
+ A5A796C22BE04E68004D0F2D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ A5A796C32BE04E68004D0F2D /* Build configuration list for PBXNativeTarget "LoggerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A5A796C42BE04E68004D0F2D /* Debug */,
+ A5A796C52BE04E68004D0F2D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = A5A796A52BE04E67004D0F2D /* Project object */;
+}
diff --git a/Logger/Logger.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Logger/Logger.xcodeproj/xcshareddata/IDETemplateMacros.plist
new file mode 100644
index 0000000..16cf018
--- /dev/null
+++ b/Logger/Logger.xcodeproj/xcshareddata/IDETemplateMacros.plist
@@ -0,0 +1,17 @@
+
+
+
+
+ FILEHEADER
+
+// ___FILENAME___
+// ___PACKAGENAME___
+//
+// Copyright (c) ___YEAR___ Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+
diff --git a/Logger/Logger.xcodeproj/xcshareddata/xcschemes/Logger.xcscheme b/Logger/Logger.xcodeproj/xcshareddata/xcschemes/Logger.xcscheme
new file mode 100644
index 0000000..1e77331
--- /dev/null
+++ b/Logger/Logger.xcodeproj/xcshareddata/xcschemes/Logger.xcscheme
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Logger/Logger/Logger.h b/Logger/Logger/Logger.h
new file mode 100644
index 0000000..38b776f
--- /dev/null
+++ b/Logger/Logger/Logger.h
@@ -0,0 +1,21 @@
+//
+// Logger.h
+// Logger
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+#import
+
+//! Project version number for Logger.
+FOUNDATION_EXPORT double LoggerVersionNumber;
+
+//! Project version string for Logger.
+FOUNDATION_EXPORT const unsigned char LoggerVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+
+
diff --git a/Logger/Logger/Logger.swift b/Logger/Logger/Logger.swift
new file mode 100644
index 0000000..079ea53
--- /dev/null
+++ b/Logger/Logger/Logger.swift
@@ -0,0 +1,46 @@
+//
+// Logger.swift
+// PingLogger
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Logger protocol that provides methods for logging different levels of information.
+public protocol Logger {
+ /// Logs a debug message.
+ /// - Parameter message: The debug message to be logged.
+ func d(_ message: String)
+
+ /// Logs an informational message.
+ /// - Parameter message: The message to be logged.
+ func i(_ message: String)
+
+ /// Logs a warning message.
+ /// - Parameters:
+ /// - message: The warning message to be logged.
+ /// - error: Optional Error associated with the warning.
+ func w(_ message: String, error: Error?)
+
+ /// Logs an error message.
+ /// - Parameters:
+ /// - message: The error message to be logged.
+ /// - error: Optional Error associated with the warning.
+ func e(_ message: String, error: Error?)
+}
+
+/// LogManager to access the global logger instances
+public struct LogManager {
+ private static var shared: Logger = NoneLogger()
+
+ /// Global logger instance. If no logger is set, it defaults to `NoneLogger()`.
+ public static var logger: Logger {
+ get { shared }
+ set { shared = newValue }
+ }
+}
diff --git a/Logger/Logger/NoneLogger.swift b/Logger/Logger/NoneLogger.swift
new file mode 100644
index 0000000..a1eacd5
--- /dev/null
+++ b/Logger/Logger/NoneLogger.swift
@@ -0,0 +1,41 @@
+//
+// NoneLogger.swift
+// PingLogger
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// The NoneLogger class is an implementation of the Logger interface that performs no operations.
+/// This can be used as a default or placeholder logger.
+public class NoneLogger: Logger {
+ /// Logs a debug message.
+ /// - Parameter message: The debug message to be logged.
+ public func d(_ message: String) {}
+
+ /// Logs an informational message.
+ /// - Parameter message: The message to be logged.
+ public func i(_ message: String) {}
+
+ /// Logs a warning message.
+ /// - Parameters:
+ /// - message: The warning message to be logged.
+ /// - error: Optional Error associated with the warning.
+ public func w(_ message: String, error: Error?) {}
+
+ /// Logs an error message.
+ /// - Parameters:
+ /// - message: The error message to be logged.
+ /// - error: Optional Error associated with the warning.
+ public func e(_ message: String, error: Error?) {}
+}
+
+extension LogManager {
+ /// Static logger of `NoneLogger` type
+ public static var none: Logger { return NoneLogger() }
+}
diff --git a/Logger/Logger/PrivacyInfo.xcprivacy b/Logger/Logger/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..fcfc9b9
--- /dev/null
+++ b/Logger/Logger/PrivacyInfo.xcprivacy
@@ -0,0 +1,10 @@
+
+
+
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTracking
+
+
+
diff --git a/Logger/Logger/StandardLogger.swift b/Logger/Logger/StandardLogger.swift
new file mode 100644
index 0000000..ef87af1
--- /dev/null
+++ b/Logger/Logger/StandardLogger.swift
@@ -0,0 +1,77 @@
+//
+// StandardLogger.swift
+// PingLogger
+//
+// Copyright (c) 2024 Ping Identity. 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 os.log
+
+/// Stadard Logger to log to the iOS Console
+public class StandardLogger: Logger {
+ /// SDK Version to be updated with each release
+ private let sdkVersion = "Ping SDK 1.0.0"
+ var log: OSLog
+
+ /// Initializer for StandardLogger
+ /// - Parameter log: Optional OSLog. Default: `OSLog(subsystem: "com.pingidentity.ios", category: "Standard")`
+ public init (log: OSLog? = nil) {
+ self.log = log ?? OSLog(subsystem: "com.pingidentity.ios", category: "Standard")
+ }
+
+ /// Logs a debug message.
+ /// - Parameter message: The debug message to be logged.
+ public func d(_ message: String) {
+ logMessage(message, log: log, type: .debug, error: nil)
+ }
+
+ /// Logs an informational message.
+ /// - Parameter message: The message to be logged.
+ public func i(_ message: String) {
+ logMessage(message, log: log, type: .info, error: nil)
+ }
+
+ /// Logs a warning message.
+ /// - Parameters:
+ /// - message: The warning message to be logged.
+ /// - error: Optional Error associated with the warning.
+ public func w(_ message: String, error: Error?) {
+ logMessage(message, log: log, type: .error, error: error)
+ }
+
+ /// Logs an error message.
+ /// - Parameters:
+ /// - message: The error message to be logged.
+ /// - error: Optional Error associated with the warning.
+ public func e(_ message: String, error: Error?) {
+ logMessage(message, log: log, type: .fault, error: error)
+ }
+
+ private func logMessage(_ message: String, log: OSLog = .default, type: OSLogType = .default, error: Error? = nil) {
+ let errorMessage = (error == nil ? "" : ", Error: \(error!.localizedDescription)")
+ os_log("%{public}@", log: log, type: type, "[\(sdkVersion)] \(message)\(errorMessage)")
+ }
+}
+
+/// Warning Logger that only logs warnings and errors
+public class WarningLogger: StandardLogger {
+ /// Logs a debug message. This implementation does nothing.
+ /// - Parameter message: The debug message to be logged.
+ public override func d(_ message: String) { }
+
+ /// Logs an info message. This implementation does nothing.
+ /// - Parameter message: The informational message to be logged.
+ public override func i(_ message: String) { }
+}
+
+extension LogManager {
+ /// Static logger of `StandardLogger` type
+ public static var standard: Logger { return StandardLogger() }
+ /// Static logger of `WarningLogger` type
+ public static var warning: Logger { return WarningLogger() }
+}
diff --git a/Logger/LoggerTests/LoggerTests.swift b/Logger/LoggerTests/LoggerTests.swift
new file mode 100644
index 0000000..10f223d
--- /dev/null
+++ b/Logger/LoggerTests/LoggerTests.swift
@@ -0,0 +1,86 @@
+//
+// LoggerTests.swift
+// LoggerTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingLogger
+
+final class LoggerTests: XCTestCase {
+
+ // TestRailCase(22062, 22063, 22064, 22065)
+ func testLoggerSetAndGet() {
+ let noneLogger = NoneLogger()
+ LogManager.logger = noneLogger
+ XCTAssert(LogManager.logger is NoneLogger)
+
+ let standardLogger = StandardLogger()
+ LogManager.logger = standardLogger
+ XCTAssert(LogManager.logger is StandardLogger)
+
+ let warningLogger = WarningLogger()
+ LogManager.logger = warningLogger
+ XCTAssert(LogManager.logger is WarningLogger)
+ }
+
+
+ func testDefaultLoggers() {
+ let noneLogger = LogManager.none
+ XCTAssert(noneLogger is NoneLogger)
+
+ let standardLogger = LogManager.standard
+ XCTAssert(standardLogger is StandardLogger)
+
+ let warningLogger = LogManager.warning
+ XCTAssert(warningLogger is WarningLogger)
+ }
+
+
+ // TestRailCase(24702)
+ func testCustomLogger() {
+ var customLogger = LogManager.customLogger
+ XCTAssert(customLogger is CustomLogger)
+
+ customLogger = CustomLogger()
+ LogManager.logger = customLogger
+ XCTAssert(LogManager.logger is CustomLogger)
+ }
+
+}
+
+struct CustomLogger: Logger {
+
+ func i(_ message: String) {
+ }
+
+ func d(_ message: String) {
+ }
+
+ func w(_ message: String, error: Error?) {
+ if let error = error {
+ print("\(message): \(error)")
+ } else {
+ print(message)
+ }
+ }
+
+ func e(_ message: String, error: Error?) {
+ if let error = error {
+ print("\(message): \(error)")
+ } else {
+ print(message)
+ }
+ }
+}
+
+extension LogManager {
+ static var customLogger: Logger {
+ return CustomLogger()
+ }
+}
diff --git a/Logger/README.md b/Logger/README.md
new file mode 100644
index 0000000..45844e5
--- /dev/null
+++ b/Logger/README.md
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+# PingLogger SDK
+
+The PingLogger SDK provides a versatile logging interface and a set of common loggers for the Ping
+SDKs.
+
+## Integrating the SDK into your project
+
+Use Cocoapods or Swift Package Manger
+
+## How to Use the SDK
+
+### Logging to the iOS Console
+
+To log messages to the console, use the `standard` logger:
+
+```swift
+import PingLogger
+
+let logger = LogManager.standard
+logger.i("Hello World")
+```
+
+With the default the log will Tag with the SDK Version:
+```
+Ping SDK
+```
+
+### Disabling Logging
+
+The PingLogger SDK provides a `none` logger that does not log any messages:
+
+```swift
+import PingLogger
+
+let logger = LogManager.none
+logger.i("Hello World") // This message will not be logged
+```
+
+### Creating a Custom Logger
+
+You can create a custom logger to suit your specific needs. For example, here's how to create a
+logger that only logs
+warning and error messages:
+
+```swift
+struct WarningErrorOnlyLogger: Logger {
+
+ func i(_ message: String) {
+ }
+
+ func d(_ message: String) {
+ }
+
+ func w(_ message: String, error: Error?) {
+ if let error = error {
+ print("\(message): \(error)")
+ } else {
+ print(message)
+ }
+ }
+
+ func e(_ message: String, error: Error?) {
+ if let error = error {
+ print("\(message): \(error)")
+ } else {
+ print(message)
+ }
+ }
+}
+
+extension LogManager {
+ static var warningErrorOnly: Logger {
+ return WarningErrorOnlyLogger()
+ }
+}
+```
+
+To use your custom logger:
+
+```swift
+let logger = LogManager.warningErrorOnly
+logger.i("Hello World") // This message will not be logged
+```
+
+## Shared Logger
+
+LogManager also provides a global shared logger: `LogManager.logger`. Default value for the `LogManager.logger` is `none`, however any type conforming to `Logger` protocol can be assigned to it, inluding the `standard` and `warning` loggers and any custom logger.
+
+## Available Loggers
+
+The PingLogger SDK provides the following loggers:
+
+| Logger | Description |
+|----------|-------------------------------------------------------|
+| standard | Logs messages to iOS Console |
+| warning | Logs warning and error messages to iOS Console |
+| none | Disables logging |
diff --git a/Oidc/Oidc.xcodeproj/project.pbxproj b/Oidc/Oidc.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..3fd8dee
--- /dev/null
+++ b/Oidc/Oidc.xcodeproj/project.pbxproj
@@ -0,0 +1,592 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 63;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 3AB1CA102BD6F99C003FCE3C /* PingOidc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AB1CA072BD6F99C003FCE3C /* PingOidc.framework */; };
+ 3AB1CA162BD6F99C003FCE3C /* Oidc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3AB1CA0A2BD6F99C003FCE3C /* Oidc.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 3AB1CA2D2BD7727B003FCE3C /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA2A2BD7727B003FCE3C /* Token.swift */; };
+ 3AB1CA2E2BD7727B003FCE3C /* Pkce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA2B2BD7727B003FCE3C /* Pkce.swift */; };
+ 3AB1CA312BD859BC003FCE3C /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA302BD859BC003FCE3C /* User.swift */; };
+ 3AB1CA332BD8823C003FCE3C /* Agent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA322BD8823C003FCE3C /* Agent.swift */; };
+ 3AB1CA352BD884E4003FCE3C /* OidcClientConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA342BD884E4003FCE3C /* OidcClientConfig.swift */; };
+ 3AB1CA3B2BD96089003FCE3C /* OidcUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA3A2BD96089003FCE3C /* OidcUser.swift */; };
+ 3AB1CA3D2BD961A2003FCE3C /* OidcClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1CA3C2BD961A2003FCE3C /* OidcClient.swift */; };
+ A50966B62C4C342800A4E5B5 /* OidcError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966B52C4C342800A4E5B5 /* OidcError.swift */; };
+ A50966B82C4C34CB00A4E5B5 /* AuthCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966B72C4C34CB00A4E5B5 /* AuthCode.swift */; };
+ A50966BA2C4C3E7300A4E5B5 /* OpenIdConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966B92C4C3E7300A4E5B5 /* OpenIdConfiguration.swift */; };
+ A50966D52C4DD79000A4E5B5 /* TokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966D42C4DD79000A4E5B5 /* TokenTests.swift */; };
+ A50966DB2C4DDFAE00A4E5B5 /* PkceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966DA2C4DDFAE00A4E5B5 /* PkceTests.swift */; };
+ A50966DD2C4DE4D700A4E5B5 /* MockResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966DC2C4DE4D700A4E5B5 /* MockResponse.swift */; };
+ A50966DF2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966DE2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift */; };
+ A50966E12C4F1D9D00A4E5B5 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966E02C4F1D9D00A4E5B5 /* MockURLProtocol.swift */; };
+ A50966E32C4F2F4300A4E5B5 /* OidcClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966E22C4F2F4300A4E5B5 /* OidcClientTests.swift */; };
+ A50966EF2C4FF75400A4E5B5 /* MockStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50966EE2C4FF75400A4E5B5 /* MockStorage.swift */; };
+ A50967092C50843E00A4E5B5 /* MockAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50967082C50843E00A4E5B5 /* MockAPIEndpoint.swift */; };
+ A50981BE2CEBDF1700F4B487 /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A50981BD2CEBDF1700F4B487 /* PingOrchestrate.framework */; };
+ A50981BF2CEBDF1700F4B487 /* PingOrchestrate.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A50981BD2CEBDF1700F4B487 /* PingOrchestrate.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A5A712442CAC520500B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A712432CAC520500B7DD58 /* PrivacyInfo.xcprivacy */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 3AB1CA112BD6F99C003FCE3C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3AB1C9FE2BD6F99C003FCE3C /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 3AB1CA062BD6F99C003FCE3C;
+ remoteInfo = PingOidc;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ A50981C02CEBDF1700F4B487 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ A50981BF2CEBDF1700F4B487 /* PingOrchestrate.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 3AB1CA072BD6F99C003FCE3C /* PingOidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingOidc.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3AB1CA0A2BD6F99C003FCE3C /* Oidc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Oidc.h; sourceTree = ""; };
+ 3AB1CA0F2BD6F99C003FCE3C /* OidcTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OidcTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3AB1CA2A2BD7727B003FCE3C /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; };
+ 3AB1CA2B2BD7727B003FCE3C /* Pkce.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pkce.swift; sourceTree = ""; };
+ 3AB1CA302BD859BC003FCE3C /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
+ 3AB1CA322BD8823C003FCE3C /* Agent.swift */ = {isa = PBXFileReference; indentWidth = 5; lastKnownFileType = sourcecode.swift; path = Agent.swift; sourceTree = ""; };
+ 3AB1CA342BD884E4003FCE3C /* OidcClientConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcClientConfig.swift; sourceTree = ""; };
+ 3AB1CA3A2BD96089003FCE3C /* OidcUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcUser.swift; sourceTree = ""; };
+ 3AB1CA3C2BD961A2003FCE3C /* OidcClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcClient.swift; sourceTree = ""; };
+ A50966B52C4C342800A4E5B5 /* OidcError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcError.swift; sourceTree = ""; };
+ A50966B72C4C34CB00A4E5B5 /* AuthCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCode.swift; sourceTree = ""; };
+ A50966B92C4C3E7300A4E5B5 /* OpenIdConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenIdConfiguration.swift; sourceTree = ""; };
+ A50966D42C4DD79000A4E5B5 /* TokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTests.swift; sourceTree = ""; };
+ A50966DA2C4DDFAE00A4E5B5 /* PkceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PkceTests.swift; sourceTree = ""; };
+ A50966DC2C4DE4D700A4E5B5 /* MockResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockResponse.swift; sourceTree = ""; };
+ A50966DE2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcClientConfigTests.swift; sourceTree = ""; };
+ A50966E02C4F1D9D00A4E5B5 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; };
+ A50966E22C4F2F4300A4E5B5 /* OidcClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OidcClientTests.swift; sourceTree = ""; };
+ A50966EE2C4FF75400A4E5B5 /* MockStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStorage.swift; sourceTree = ""; };
+ A50967082C50843E00A4E5B5 /* MockAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIEndpoint.swift; sourceTree = ""; };
+ A50981BD2CEBDF1700F4B487 /* PingOrchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOrchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A5A712432CAC520500B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 3AB1CA042BD6F99C003FCE3C /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A50981BE2CEBDF1700F4B487 /* PingOrchestrate.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3AB1CA0C2BD6F99C003FCE3C /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3AB1CA102BD6F99C003FCE3C /* PingOidc.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 3A8532272BE2C5DB00F8619D /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ A50981BD2CEBDF1700F4B487 /* PingOrchestrate.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 3AB1C9FD2BD6F99C003FCE3C = {
+ isa = PBXGroup;
+ children = (
+ 3AB1CA092BD6F99C003FCE3C /* Oidc */,
+ 3AB1CA132BD6F99C003FCE3C /* OidcTests */,
+ 3AB1CA082BD6F99C003FCE3C /* Products */,
+ 3A8532272BE2C5DB00F8619D /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 3AB1CA082BD6F99C003FCE3C /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 3AB1CA072BD6F99C003FCE3C /* PingOidc.framework */,
+ 3AB1CA0F2BD6F99C003FCE3C /* OidcTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 3AB1CA092BD6F99C003FCE3C /* Oidc */ = {
+ isa = PBXGroup;
+ children = (
+ A5A712432CAC520500B7DD58 /* PrivacyInfo.xcprivacy */,
+ 3AB1CA0A2BD6F99C003FCE3C /* Oidc.h */,
+ 3AB1CA322BD8823C003FCE3C /* Agent.swift */,
+ A50966B72C4C34CB00A4E5B5 /* AuthCode.swift */,
+ 3AB1CA3C2BD961A2003FCE3C /* OidcClient.swift */,
+ 3AB1CA342BD884E4003FCE3C /* OidcClientConfig.swift */,
+ A50966B52C4C342800A4E5B5 /* OidcError.swift */,
+ 3AB1CA3A2BD96089003FCE3C /* OidcUser.swift */,
+ A50966B92C4C3E7300A4E5B5 /* OpenIdConfiguration.swift */,
+ 3AB1CA2B2BD7727B003FCE3C /* Pkce.swift */,
+ 3AB1CA2A2BD7727B003FCE3C /* Token.swift */,
+ 3AB1CA302BD859BC003FCE3C /* User.swift */,
+ );
+ path = Oidc;
+ sourceTree = "";
+ };
+ 3AB1CA132BD6F99C003FCE3C /* OidcTests */ = {
+ isa = PBXGroup;
+ children = (
+ A509D1CC2C6D04E9003A0006 /* mock */,
+ A50966DE2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift */,
+ A50966E22C4F2F4300A4E5B5 /* OidcClientTests.swift */,
+ A50966DA2C4DDFAE00A4E5B5 /* PkceTests.swift */,
+ A50966D42C4DD79000A4E5B5 /* TokenTests.swift */,
+ );
+ path = OidcTests;
+ sourceTree = "";
+ };
+ A509D1CC2C6D04E9003A0006 /* mock */ = {
+ isa = PBXGroup;
+ children = (
+ A50967082C50843E00A4E5B5 /* MockAPIEndpoint.swift */,
+ A50966DC2C4DE4D700A4E5B5 /* MockResponse.swift */,
+ A50966E02C4F1D9D00A4E5B5 /* MockURLProtocol.swift */,
+ A50966EE2C4FF75400A4E5B5 /* MockStorage.swift */,
+ );
+ path = mock;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+ 3AB1CA022BD6F99C003FCE3C /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3AB1CA162BD6F99C003FCE3C /* Oidc.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+ 3AB1CA062BD6F99C003FCE3C /* PingOidc */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3AB1CA192BD6F99C003FCE3C /* Build configuration list for PBXNativeTarget "PingOidc" */;
+ buildPhases = (
+ 3AB1CA022BD6F99C003FCE3C /* Headers */,
+ 3AB1CA032BD6F99C003FCE3C /* Sources */,
+ 3AB1CA042BD6F99C003FCE3C /* Frameworks */,
+ 3AB1CA052BD6F99C003FCE3C /* Resources */,
+ A50981C02CEBDF1700F4B487 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PingOidc;
+ productName = PingOidc;
+ productReference = 3AB1CA072BD6F99C003FCE3C /* PingOidc.framework */;
+ productType = "com.apple.product-type.framework";
+ };
+ 3AB1CA0E2BD6F99C003FCE3C /* OidcTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3AB1CA1C2BD6F99C003FCE3C /* Build configuration list for PBXNativeTarget "OidcTests" */;
+ buildPhases = (
+ 3AB1CA0B2BD6F99C003FCE3C /* Sources */,
+ 3AB1CA0C2BD6F99C003FCE3C /* Frameworks */,
+ 3AB1CA0D2BD6F99C003FCE3C /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 3AB1CA122BD6F99C003FCE3C /* PBXTargetDependency */,
+ );
+ name = OidcTests;
+ productName = PingOidcTests;
+ productReference = 3AB1CA0F2BD6F99C003FCE3C /* OidcTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 3AB1C9FE2BD6F99C003FCE3C /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1500;
+ TargetAttributes = {
+ 3AB1CA062BD6F99C003FCE3C = {
+ CreatedOnToolsVersion = 15.0;
+ LastSwiftMigration = 1500;
+ };
+ 3AB1CA0E2BD6F99C003FCE3C = {
+ CreatedOnToolsVersion = 15.0;
+ LastSwiftMigration = 1540;
+ };
+ };
+ };
+ buildConfigurationList = 3AB1CA012BD6F99C003FCE3C /* Build configuration list for PBXProject "Oidc" */;
+ compatibilityVersion = "Xcode 15.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 3AB1C9FD2BD6F99C003FCE3C;
+ productRefGroup = 3AB1CA082BD6F99C003FCE3C /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 3AB1CA062BD6F99C003FCE3C /* PingOidc */,
+ 3AB1CA0E2BD6F99C003FCE3C /* OidcTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 3AB1CA052BD6F99C003FCE3C /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A712442CAC520500B7DD58 /* PrivacyInfo.xcprivacy in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3AB1CA0D2BD6F99C003FCE3C /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 3AB1CA032BD6F99C003FCE3C /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3AB1CA312BD859BC003FCE3C /* User.swift in Sources */,
+ 3AB1CA3D2BD961A2003FCE3C /* OidcClient.swift in Sources */,
+ 3AB1CA332BD8823C003FCE3C /* Agent.swift in Sources */,
+ A50966B82C4C34CB00A4E5B5 /* AuthCode.swift in Sources */,
+ 3AB1CA3B2BD96089003FCE3C /* OidcUser.swift in Sources */,
+ A50966BA2C4C3E7300A4E5B5 /* OpenIdConfiguration.swift in Sources */,
+ 3AB1CA2E2BD7727B003FCE3C /* Pkce.swift in Sources */,
+ A50966B62C4C342800A4E5B5 /* OidcError.swift in Sources */,
+ 3AB1CA2D2BD7727B003FCE3C /* Token.swift in Sources */,
+ 3AB1CA352BD884E4003FCE3C /* OidcClientConfig.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3AB1CA0B2BD6F99C003FCE3C /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A50967092C50843E00A4E5B5 /* MockAPIEndpoint.swift in Sources */,
+ A50966DB2C4DDFAE00A4E5B5 /* PkceTests.swift in Sources */,
+ A50966EF2C4FF75400A4E5B5 /* MockStorage.swift in Sources */,
+ A50966DF2C4DF3CD00A4E5B5 /* OidcClientConfigTests.swift in Sources */,
+ A50966E12C4F1D9D00A4E5B5 /* MockURLProtocol.swift in Sources */,
+ A50966DD2C4DE4D700A4E5B5 /* MockResponse.swift in Sources */,
+ A50966D52C4DD79000A4E5B5 /* TokenTests.swift in Sources */,
+ A50966E32C4F2F4300A4E5B5 /* OidcClientTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 3AB1CA122BD6F99C003FCE3C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 3AB1CA062BD6F99C003FCE3C /* PingOidc */;
+ targetProxy = 3AB1CA112BD6F99C003FCE3C /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 3AB1CA172BD6F99C003FCE3C /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ 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";
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Debug;
+ };
+ 3AB1CA182BD6F99C003FCE3C /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Release;
+ };
+ 3AB1CA1A2BD6F99C003FCE3C /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Oidc;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 3AB1CA1B2BD6F99C003FCE3C /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Oidc;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 3AB1CA1D2BD6F99C003FCE3C /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.OidcTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Debug;
+ };
+ 3AB1CA1E2BD6F99C003FCE3C /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.OidcTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 3AB1CA012BD6F99C003FCE3C /* Build configuration list for PBXProject "Oidc" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3AB1CA172BD6F99C003FCE3C /* Debug */,
+ 3AB1CA182BD6F99C003FCE3C /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 3AB1CA192BD6F99C003FCE3C /* Build configuration list for PBXNativeTarget "PingOidc" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3AB1CA1A2BD6F99C003FCE3C /* Debug */,
+ 3AB1CA1B2BD6F99C003FCE3C /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 3AB1CA1C2BD6F99C003FCE3C /* Build configuration list for PBXNativeTarget "OidcTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3AB1CA1D2BD6F99C003FCE3C /* Debug */,
+ 3AB1CA1E2BD6F99C003FCE3C /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 3AB1C9FE2BD6F99C003FCE3C /* Project object */;
+}
diff --git a/Oidc/Oidc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Oidc/Oidc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/Oidc/Oidc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Oidc/Oidc.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Oidc/Oidc.xcodeproj/xcshareddata/IDETemplateMacros.plist
new file mode 100644
index 0000000..16cf018
--- /dev/null
+++ b/Oidc/Oidc.xcodeproj/xcshareddata/IDETemplateMacros.plist
@@ -0,0 +1,17 @@
+
+
+
+
+ FILEHEADER
+
+// ___FILENAME___
+// ___PACKAGENAME___
+//
+// Copyright (c) ___YEAR___ Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+
diff --git a/Oidc/Oidc.xcodeproj/xcshareddata/xcschemes/Oidc.xcscheme b/Oidc/Oidc.xcodeproj/xcshareddata/xcschemes/Oidc.xcscheme
new file mode 100644
index 0000000..5817830
--- /dev/null
+++ b/Oidc/Oidc.xcodeproj/xcshareddata/xcschemes/Oidc.xcscheme
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Oidc/Oidc/Agent.swift b/Oidc/Oidc/Agent.swift
new file mode 100644
index 0000000..9205bb3
--- /dev/null
+++ b/Oidc/Oidc/Agent.swift
@@ -0,0 +1,120 @@
+//
+// Agent.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// The `Agent` is a protocol that is used to authenticate and end a session with an OpenID Connect provider.
+/// `T` is the configuration object that is used to configure the `Agent`.
+public protocol Agent {
+ associatedtype T
+
+ /// Provides the configuration object for the `Agent`.
+ /// - Returns: A function that returns the configuration object.
+ func config() -> () -> T
+
+ /// End the session with the OpenID Connect provider.
+ /// Best effort is made to end the session.
+ func endSession(oidcConfig: OidcConfig, idToken: String) async throws -> Bool
+
+ /// Authorize the `Agent` with the OpenID Connect provider.
+ /// Before returning the `AuthCode`, the agent should verify the response from the OpenID Connect provider.
+ /// For example, the agent should verify the state parameter in the response.
+ /// - Parameter oidcConfig: The configuration for the OpenID Connect client.
+ /// - Returns: `AuthCode` instance
+ func authorize(oidcConfig: OidcConfig) async throws -> AuthCode
+}
+
+
+/// Allow the `Agent` to run on `OidcConfig` so that it can access the configuration object.
+public class OidcConfig {
+ let oidcClientConfig: OidcClientConfig
+ let config: T
+
+ /// Initialize the `OidcConfig` with the `OidcClientConfig` and the configuration object.
+ /// - Parameters:
+ /// - oidcClientConfig: The client configuration for the OpenID Connect provider.
+ /// - config: The configuration object.
+ init(oidcClientConfig: OidcClientConfig, config: T) {
+ self.oidcClientConfig = oidcClientConfig
+ self.config = config
+ }
+}
+
+
+/// Default implementation of the `Agent` interface.
+public class DefaultAgent: Agent {
+
+ public typealias T = Void
+
+ /// Initialize the `DefaultAgent`.
+ public init() {}
+
+ /// Provides an empty configuration for the `DefaultAgent`.
+ /// - Returns: A function that returns Void
+ public func config() -> () -> Void {
+ return {}
+ }
+
+ /// End the session with the OpenID Connect provider. This implementation always returns false.
+ /// - Parameters:
+ /// - oidcConfig: The configuration for the OpenID Connect client.
+ /// - idToken: The ID token used to end the session.
+ /// - Returns: Always returns false.
+ public func endSession(oidcConfig: OidcConfig, idToken: String) async -> Bool {
+ return false
+ }
+
+ /// Authorize the `DefaultAgent` with the OpenID Connect provider.
+ /// This implementation always throws an `OidcError.authorizeError` error.
+ /// - Parameter oidcConfig: The configuration for the OpenID Connect client.
+ /// - Returns: Never returns normally.
+ public func authorize(oidcConfig: OidcConfig) async throws -> AuthCode {
+ throw OidcError.authorizeError(message: "No AuthCode is available.")
+ }
+}
+
+
+/// Delegate protocol to dispatch `Agent` functions
+public protocol AgentDelegateProtocol {
+ associatedtype T
+ func authenticate() async throws -> AuthCode
+ func endSession(idToken: String) async throws -> Bool
+}
+
+
+/// Delegate class to dispatch `Agent` functions
+public class AgentDelegate: AgentDelegateProtocol {
+ let agent: any Agent
+ let oidcConfig: OidcConfig
+
+ /// Initialize the `AgentDelegate` with an `Agent` and the configuration object.
+ /// - Parameters:
+ /// - agent: The `Agent` instance.
+ /// - agentConfig: The configuration object for the `Agent`.
+ /// - oidcClientConfig: The `OidcClientConfig` instance .
+ init(agent: any Agent, agentConfig: T, oidcClientConfig: OidcClientConfig) {
+ self.agent = agent
+ self.oidcConfig = OidcConfig(oidcClientConfig: oidcClientConfig, config: agentConfig)
+ }
+
+ /// Authenticate with the OpenID Connect provider.
+ /// - Returns: The authorization code.
+ public func authenticate() async throws -> AuthCode {
+ return try await self.agent.authorize(oidcConfig: oidcConfig)
+ }
+
+ /// End the session with the OpenID Connect provider.
+ /// - Parameter idToken: The ID token used to end the session.
+ /// - Returns: A boolean indicating whether the session was successfully ended.
+ public func endSession(idToken: String) async throws -> Bool {
+ return try await agent.endSession(oidcConfig: oidcConfig, idToken: idToken)
+ }
+}
diff --git a/Oidc/Oidc/AuthCode.swift b/Oidc/Oidc/AuthCode.swift
new file mode 100644
index 0000000..192dc54
--- /dev/null
+++ b/Oidc/Oidc/AuthCode.swift
@@ -0,0 +1,27 @@
+//
+// AuthCode.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Struct representing an authorization code.
+public struct AuthCode: Codable {
+ let code: String
+ let codeVerifier: String?
+
+ /// Initializes a new instance of `AuthCode`.
+ /// - Parameters:
+ /// - code: The authorization code as a string. Default is an empty string.
+ /// - codeVerifier: An optional code verifier associated with the authorization code. Default is `nil`.
+ public init(code: String = "", codeVerifier: String? = nil) {
+ self.code = code
+ self.codeVerifier = codeVerifier
+ }
+}
diff --git a/Oidc/Oidc/Oidc.h b/Oidc/Oidc/Oidc.h
new file mode 100644
index 0000000..8f9781a
--- /dev/null
+++ b/Oidc/Oidc/Oidc.h
@@ -0,0 +1,21 @@
+//
+// Oidc.h
+// Oidc
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+#import
+
+//! Project version number for Oidc.
+FOUNDATION_EXPORT double OidcVersionNumber;
+
+//! Project version string for Oidc.
+FOUNDATION_EXPORT const unsigned char OidcVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+
+
diff --git a/Oidc/Oidc/OidcClient.swift b/Oidc/Oidc/OidcClient.swift
new file mode 100644
index 0000000..8a2bd4c
--- /dev/null
+++ b/Oidc/Oidc/OidcClient.swift
@@ -0,0 +1,267 @@
+//
+// OidcClient.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. 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 PingLogger
+import PingOrchestrate
+
+/// Class representing an OpenID Connect client.
+public class OidcClient {
+ private let config: OidcClientConfig
+ private let logger: Logger
+
+ /// OidcClient initializer.
+ /// - Parameter config: The configuration for this client.
+ public init(config: OidcClientConfig) {
+ self.config = config
+ self.logger = config.logger
+ }
+
+ /// Retrieves an access token. If a cached token is available and not expired, it is returned.
+ /// Otherwise, a new token is fetched with refresh token if refresh grant is available.
+ /// - Returns: A Result containing the access token or an error.
+ public func token() async -> Result {
+
+ do {
+ try await config.oidcInitialize()
+ } catch {
+ return .failure((error as? OidcError) ?? OidcError.unknown(cause: error))
+ }
+
+ config.logger.i("Getting access token")
+ if let cached = try? await config.storage.get() {
+ if !cached.isExpired(threshold: config.refreshThreshold) {
+ config.logger.i("Token is not expired. Returning cached token.")
+ return .success(cached)
+ }
+ config.logger.i("Token is expired. Attempting to refresh.")
+ if let cachedefreshToken = cached.refreshToken {
+ do {
+ let refreshedToken = try await refreshToken(cachedefreshToken)
+ return .success(refreshedToken)
+ } catch {
+ config.logger.e("Failed to refresh token. Revoking token and re-authenticating.", error: error)
+ await revoke(cached)
+ }
+ }
+ }
+
+ // Authenticate the user
+ do {
+ let code = try await config.agent?.authenticate()
+ if let unWrappedcode = code {
+ let token = try await exchangeToken(unWrappedcode)
+ try await config.storage.save(item: token)
+ return .success(token)
+ } else {
+ return .failure(OidcError.authorizeError(message: "Authorization code not found"))
+ }
+
+ } catch {
+ return .failure((error as? OidcError) ?? (OidcError.authorizeError(cause: error)))
+ }
+ }
+
+ /// Refreshes the access token.
+ /// - Parameter refreshToken: The refresh token to use for refreshing the access token.
+ /// - Returns: The refreshed access token.
+ private func refreshToken(_ refreshToken: String) async throws -> Token {
+ try await config.oidcInitialize()
+ config.logger.i("Refreshing token")
+
+ let params = [
+ Constants.grant_type: Constants.refresh_token,
+ Constants.refresh_token: refreshToken,
+ Constants.client_id: config.clientId
+ ]
+
+ guard let httpClient = config.httpClient else {
+ throw OidcError.networkError(message: "HTTP client not found")
+ }
+
+ guard let openId = config.openId else {
+ throw OidcError.unknown(message: "OpenID configuration not found")
+ }
+
+ let request = Request()
+ request.url(openId.tokenEndpoint)
+ request.form(formData: params)
+
+ let (data, response) = try await httpClient.sendRequest(request: request)
+
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
+ throw OidcError.apiError(code: (response as? HTTPURLResponse)?.statusCode ?? 0, message: String(decoding: data, as: UTF8.self))
+ }
+ let token = try JSONDecoder().decode(Token.self, from: data)
+
+ try await config.storage.save(item: token)
+
+ return token
+ }
+
+ /// Revokes the access token.
+ public func revoke() async {
+ await revoke(nil)
+ }
+
+ /// Revokes a specific access token. Best effort to revoke the token.
+ /// The stored token is removed regardless of the result.
+ /// - Parameter token: The access token to revoke. If null, the currently stored token is revoked.
+ private func revoke(_ token: Token? = nil) async {
+ var accessToken = token
+ if accessToken == nil {
+ accessToken = try? await config.storage.get()
+ }
+ if let token = accessToken {
+ do {
+ try await config.storage.delete()
+ try await config.oidcInitialize()
+ } catch {
+ config.logger.e("Failed to delete token", error: error)
+ }
+ let t = token.refreshToken ?? token.accessToken
+ let params = [
+ Constants.client_id: config.clientId,
+ Constants.token: t
+ ]
+
+ guard let httpClient = config.httpClient else {
+ config.logger.e("HTTP client not found", error: nil)
+ return
+ }
+
+ guard let openId = config.openId else {
+ config.logger.e("OpenID configuration not found", error: nil)
+ return
+ }
+
+ let request = Request()
+ request.url(openId.revocationEndpoint)
+ request.form(formData: params)
+ do {
+ let (_, _) = try await httpClient.sendRequest(request: request)
+ } catch {
+ config.logger.e("Failed to revoke token", error: error)
+ }
+ }
+ }
+
+ /// Ends the session. Best effort to end the session.
+ /// The stored token is removed regardless of the result.
+ /// - Returns: A boolean indicating whether the session was ended successfully.
+ public func endSession() async -> Bool {
+ return await endSession { idToken in
+ return try await self.config.agent?.endSession(idToken: idToken) ?? false
+ }
+ }
+
+ /// Ends the session with a custom sign-off procedure.
+ /// - Parameter signOff: A suspend function to perform the sign-off.
+ /// - Returns: A boolean indicating whether the session was ended successfully.
+ public func endSession(signOff: @escaping (String) async throws -> Bool) async -> Bool {
+ do {
+ try await config.oidcInitialize()
+ if let accessToken = try await config.storage.get() {
+ await revoke(accessToken)
+ if let idToken = accessToken.idToken {
+ return try await signOff(idToken)
+ }
+ }
+ } catch {
+ config.logger.e("Failed to end session", error: error)
+ return false
+ }
+ return true
+ }
+
+ /// Retrieves user information.
+ /// - Returns: A Result containing the user information or an error.
+ public func userinfo() async -> Result {
+ do {
+ try await config.oidcInitialize()
+
+ guard let httpClient = config.httpClient else {
+ throw OidcError.networkError(message: "HTTP client not found") }
+
+ guard let openId = config.openId else {
+ throw OidcError.unknown(message: "OpenID configuration not found")
+ }
+
+ switch await token() {
+ case .failure(let error):
+ return .failure(error)
+ case .success(let token):
+ let request = Request()
+ request.url(openId.userinfoEndpoint)
+ request.header(name: "Authorization", value: "Bearer \(token.accessToken)")
+ let (data, response) = try await httpClient.sendRequest(request: request)
+
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
+ throw OidcError.apiError(code: (response as? HTTPURLResponse)?.statusCode ?? 0, message: String(decoding: data, as: UTF8.self))
+ }
+ let json = try JSONSerialization.jsonObject(with: data, options: []) as? UserInfo ?? [:]
+ return .success(json)
+ }
+ } catch {
+ return .failure((error as? OidcError) ?? .unknown(cause: error))
+ }
+ }
+
+ /// Exchanges an authorization code for an access token.
+ /// - Parameter authCode: The authorization code to exchange.
+ /// - Returns: The access token.
+ private func exchangeToken(_ authCode: AuthCode) async throws -> Token {
+ try await config.oidcInitialize()
+ config.logger.i("Exchanging token")
+
+ guard let httpClient = config.httpClient else {
+ throw OidcError.networkError(message: "HTTP client not found")
+ }
+
+ guard let openId = config.openId else {
+ throw OidcError.unknown(message: "OpenID configuration not found")
+ }
+
+ var params = [
+ Constants.grant_type: Constants.authorization_code,
+ Constants.code: authCode.code,
+ Constants.redirect_uri: config.redirectUri,
+ Constants.client_id: config.clientId,
+ ]
+
+ if let codeVerifier = authCode.codeVerifier {
+ params[Constants.code_verifier] = codeVerifier
+ }
+
+ let request = Request()
+ request.url(openId.tokenEndpoint)
+ request.form(formData: params)
+ let (data, response) = try await httpClient.sendRequest(request: request)
+
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
+ throw OidcError.apiError(code: (response as? HTTPURLResponse)?.statusCode ?? 0, message: String(decoding: data, as: UTF8.self))
+ }
+ let token = try JSONDecoder().decode(Token.self, from: data)
+ return token
+ }
+
+ public enum Constants {
+ public static let client_id = "client_id"
+ public static let grant_type = "grant_type"
+ public static let refresh_token = "refresh_token"
+ public static let token = "token"
+ public static let authorization_code = "authorization_code"
+ public static let redirect_uri = "redirect_uri"
+ public static let code_verifier = "code_verifier"
+ public static let code = "code"
+ public static let id_token_hint = "id_token_hint"
+ }
+}
diff --git a/Oidc/Oidc/OidcClientConfig.swift b/Oidc/Oidc/OidcClientConfig.swift
new file mode 100644
index 0000000..5f8ffd6
--- /dev/null
+++ b/Oidc/Oidc/OidcClientConfig.swift
@@ -0,0 +1,143 @@
+//
+// OidcClientConfig.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. 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 PingOrchestrate
+import PingLogger
+import PingStorage
+
+/// Configuration class for OIDC client.
+public class OidcClientConfig {
+ /// OpenID configuration.
+ public var openId: OpenIdConfiguration?
+ /// Token refresh threshold in seconds.
+ public var refreshThreshold: Int64 = 0
+ /// Agent delegate for handling OIDC operations.
+ internal var agent: (any AgentDelegateProtocol)?
+ /// Logger instance for logging.
+ public var logger: Logger
+ /// Storage delegate for storing tokens.
+ public var storage: StorageDelegate
+ /// Discovery endpoint URL.
+ public var discoveryEndpoint = ""
+ /// Client ID for OIDC.
+ public var clientId = ""
+ /// Set of scopes for OIDC.
+ public var scopes = Set()
+ /// Redirect URI for OIDC.
+ public var redirectUri = ""
+ /// Login hint for OIDC.
+ public var loginHint: String?
+ /// State parameter for OIDC.
+ public var state: String?
+ /// Nonce parameter for OIDC.
+ public var nonce: String?
+ /// Display parameter for OIDC.
+ public var display: String?
+ /// Prompt parameter for OIDC.
+ public var prompt: String?
+ /// UI locales parameter for OIDC.
+ public var uiLocales: String?
+ /// ACR values parameter for OIDC.
+ public var acrValues: String?
+ /// Additional parameters for OIDC.
+ public var additionalParameters = [String: String]()
+ /// HTTP client for making network requests.
+ public var httpClient: HttpClient?
+
+ /// Initializes a new `OidcClientConfig` instance.
+ public init() {
+ logger = LogManager.none
+ storage = KeychainStorage(account: "ACCESS_TOKEN_STORAGE", encryptor: SecuredKeyEncryptor() ?? NoEncryptor(), cacheable: true)
+ }
+
+ /// Adds a scope to the set of scopes.
+ /// - Parameter scope: The scope to add.
+ public func scope(_ scope: String) {
+ scopes.insert(scope)
+ }
+
+ /// Updates the agent with the provided configuration.
+ /// - Parameters:
+ /// - agent: The agent to update.
+ /// - config: The configuration block for the agent.
+ public func updateAgent(_ agent: any Agent, config: (T) -> Void = {_ in }) {
+ self.agent = AgentDelegate(agent: agent, agentConfig: agent.config()(), oidcClientConfig: self)
+
+ }
+
+ /// Initializes the lazy properties to their default values.
+ public func oidcInitialize() async throws {
+ if httpClient == nil {
+ httpClient = HttpClient()
+ }
+
+ if openId != nil {
+ return
+ }
+
+ openId = try await discover()
+ }
+
+ /// Discovers the OpenID configuration from the discovery endpoint.
+ /// - Returns: The discovered OpenID configuration.
+ private func discover() async throws -> OpenIdConfiguration? {
+ guard URL(string: discoveryEndpoint) != nil else {
+ logger.e("Invalid Discovery URL", error: nil)
+ return nil
+ }
+
+ guard let httpClient else {
+ logger.e("Invalid Http Client URL", error: nil)
+ return nil
+ }
+ let request = Request()
+ request.url(discoveryEndpoint)
+ let (data, response) = try await httpClient.sendRequest(request: request)
+
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
+ throw OidcError.apiError(code: (response as? HTTPURLResponse)?.statusCode ?? 500, message: String(decoding: data, as: UTF8.self))
+ }
+ let configuration = try JSONDecoder().decode(OpenIdConfiguration.self, from: data)
+ return configuration
+ }
+
+ /// Clones the current configuration.
+ /// - Returns: A new instance of OidcClientConfig with the same properties.
+ public func clone() -> OidcClientConfig {
+ let cloned = OidcClientConfig()
+ cloned.update(with: self)
+ return cloned
+ }
+
+ /// Merges another configuration into this one.
+ /// - Parameter other: The other configuration to merge.
+ func update(with other: OidcClientConfig) {
+ self.openId = other.openId
+ self.refreshThreshold = other.refreshThreshold
+ self.agent = other.agent
+ self.logger = other.logger
+ self.storage = other.storage
+ self.discoveryEndpoint = other.discoveryEndpoint
+ self.clientId = other.clientId
+ self.scopes = other.scopes
+ self.redirectUri = other.redirectUri
+ self.loginHint = other.loginHint
+ self.state = other.state
+ self.nonce = other.nonce
+ self.display = other.display
+ self.prompt = other.prompt
+ self.uiLocales = other.uiLocales
+ self.acrValues = other.acrValues
+ self.additionalParameters = other.additionalParameters
+ self.httpClient = other.httpClient
+ }
+}
diff --git a/Oidc/Oidc/OidcError.swift b/Oidc/Oidc/OidcError.swift
new file mode 100644
index 0000000..2c0ad60
--- /dev/null
+++ b/Oidc/Oidc/OidcError.swift
@@ -0,0 +1,54 @@
+//
+// OidcError.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Enum for OIDC errors.
+public enum OidcError: LocalizedError {
+ /// An error that occurs during the authorization process.
+ /// - Parameters:
+ /// - cause: The underlying error that caused the issue (optional).
+ /// - message: A descriptive message about the error (optional).
+ case authorizeError(cause: Error? = nil, message: String? = nil)
+
+ /// An error that occurs during network communication.
+ /// - Parameters:
+ /// - cause: The underlying error that caused the issue (optional).
+ /// - message: A descriptive message about the error (optional).
+ case networkError(cause: Error? = nil, message: String? = nil)
+
+ /// An error returned from the API.
+ /// - Parameters:
+ /// - code: The HTTP status code of the error.
+ /// - message: A descriptive message about the error.
+ case apiError(code: Int, message: String)
+
+ /// An unknown or unspecified error.
+ /// - Parameters:
+ /// - cause: The underlying error that caused the issue (optional).
+ /// - message: A descriptive message about the error (optional).
+ case unknown(cause: Error? = nil, message: String? = nil)
+
+ /// Provides a human-readable description of the error.
+ /// - Returns: A `String` representing the error message.
+ public var errorMessage: String {
+ switch self {
+ case .authorizeError(cause: let cause, message: let message):
+ return "Authorization error: \(message ?? cause?.localizedDescription ?? "Unknown")"
+ case .networkError(cause: let cause, message: let message):
+ return "Network error: \(message ?? cause?.localizedDescription ?? "Unknown")"
+ case .apiError(code: let code, message: let message):
+ return "API error: \(code) \(message)"
+ case .unknown(cause: let cause, message: let message):
+ return "Error: \(message ?? cause?.localizedDescription ?? "Unknown")"
+ }
+ }
+}
diff --git a/Oidc/Oidc/OidcUser.swift b/Oidc/Oidc/OidcUser.swift
new file mode 100644
index 0000000..8654853
--- /dev/null
+++ b/Oidc/Oidc/OidcUser.swift
@@ -0,0 +1,52 @@
+//
+// OidcUser.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+/// Class for an OIDC User
+public class OidcUser: User {
+ private var userinfo: UserInfo?
+ private let oidcClient: OidcClient
+
+ /// OidcUser initializer
+ /// - Parameter config: The configuration for the OIDC client.
+ public init(config: OidcClientConfig) {
+ self.oidcClient = OidcClient(config: config)
+ }
+
+ /// Gets the token for the user.
+ /// - Returns: The token for the user.
+ public func token() async -> Result {
+ return await oidcClient.token()
+ }
+
+ /// Revokes the user's token.
+ public func revoke() async {
+ await oidcClient.revoke()
+ }
+
+ /// Gets the user information.
+ /// - Parameter cache: Whether to cache the user information.
+ /// - Returns: The user information.
+ public func userinfo(cache: Bool = true) async -> Result {
+ if let userinfo = self.userinfo, cache {
+ return .success(userinfo)
+ }
+ let result = await oidcClient.userinfo()
+ if case .success(let data) = result, cache {
+ self.userinfo = data
+ }
+ return result
+ }
+
+ /// Logs out the user.
+ public func logout() async {
+ _ = await oidcClient.endSession()
+ }
+}
diff --git a/Oidc/Oidc/OpenIdConfiguration.swift b/Oidc/Oidc/OpenIdConfiguration.swift
new file mode 100644
index 0000000..ee6688b
--- /dev/null
+++ b/Oidc/Oidc/OpenIdConfiguration.swift
@@ -0,0 +1,35 @@
+//
+// OpenIdConfiguration.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Struct representing the OpenID Connect configuration.
+public struct OpenIdConfiguration: Codable {
+ /// The URL of the authorization endpoint.
+ public let authorizationEndpoint: String
+ /// The URL of the token endpoint.
+ public let tokenEndpoint: String
+ /// The URL of the userinfo endpoint.
+ public let userinfoEndpoint: String
+ /// The URL of the end session endpoint.
+ public let endSessionEndpoint: String
+ /// The URL of the revocation endpoint .
+ public let revocationEndpoint: String
+
+ // Define CodingKeys enum to map serialized names to property names
+ private enum CodingKeys: String, CodingKey {
+ case authorizationEndpoint = "authorization_endpoint"
+ case tokenEndpoint = "token_endpoint"
+ case userinfoEndpoint = "userinfo_endpoint"
+ case endSessionEndpoint = "end_session_endpoint"
+ case revocationEndpoint = "revocation_endpoint"
+ }
+}
diff --git a/Oidc/Oidc/Pkce.swift b/Oidc/Oidc/Pkce.swift
new file mode 100644
index 0000000..27c2784
--- /dev/null
+++ b/Oidc/Oidc/Pkce.swift
@@ -0,0 +1,61 @@
+//
+// PKCE.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. 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 CryptoKit
+
+/// Struct for PKCE (Proof Key for Code Exchange).
+/// - property codeVerifier: The code verifier for the PKCE.
+/// - property codeChallenge: The code challenge for the PKCE.
+/// - property codeChallengeMethod: The code challenge method for the PKCE.
+public struct Pkce {
+ public let codeVerifier: String
+ public let codeChallenge: String
+ public let codeChallengeMethod: String
+
+ /// Generates a new PKCE.
+ /// - Returns: A new PKCE.
+ public static func generate() -> Pkce {
+ let codeVerifier = generateCodeVerifier()
+ let codeChallenge = generateCodeChallenge(codeVerifier: codeVerifier)
+ return Pkce(codeVerifier: codeVerifier, codeChallenge: codeChallenge, codeChallengeMethod: "S256")
+ }
+
+ /// Generates a new code verifier for the PKCE.
+ /// - returns: A new code verifier.
+ private static func generateCodeVerifier() -> String {
+ var bytes = [UInt8](repeating: 0, count: 64)
+ _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
+ return Data(bytes).base64URLEncodedString() // remove padding as per https://tools.ietf.org/html/rfc7636#section-4.1
+ }
+
+ /// Generates a new code challenge for the PKCE.
+ /// - Parameter codeVerifier: The code verifier for the PKCE.
+ /// - Returns: A new code challenge.
+ private static func generateCodeChallenge(codeVerifier: String) -> String {
+ guard let data = codeVerifier.data(using: .utf8) else {
+ fatalError("Unable to convert code verifier to data")
+ }
+ let digest = SHA256.hash(data: data)
+ return Data(digest).base64URLEncodedString() // remove padding as per https://tools.ietf.org/html/rfc7636#section-4.1
+ }
+}
+
+
+extension Data {
+ func base64URLEncodedString() -> String {
+ return base64EncodedString()
+ .replacingOccurrences(of: "+", with: "-")
+ .replacingOccurrences(of: "/", with: "_")
+ .replacingOccurrences(of: "=", with: "")
+ .trimmingCharacters(in: .whitespaces)
+ }
+}
diff --git a/Oidc/Oidc/PrivacyInfo.xcprivacy b/Oidc/Oidc/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..fcfc9b9
--- /dev/null
+++ b/Oidc/Oidc/PrivacyInfo.xcprivacy
@@ -0,0 +1,10 @@
+
+
+
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTracking
+
+
+
diff --git a/Oidc/Oidc/Token.swift b/Oidc/Oidc/Token.swift
new file mode 100644
index 0000000..cc4e489
--- /dev/null
+++ b/Oidc/Oidc/Token.swift
@@ -0,0 +1,112 @@
+//
+// Token.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Struct representing an OIDC token.
+public struct Token: Codable {
+ /// The access token used for authentication.
+ public let accessToken: String
+ /// The type of token.
+ public let tokenType: String?
+ /// The scope of access granted by the token.
+ public let scope: String?
+ /// The duration of the token's validity in seconds
+ public let expiresIn: Int64
+ /// The refresh token used to obtain a new access token.
+ public let refreshToken: String?
+ /// The ID token
+ public let idToken: String?
+ /// The exact timestamp (in seconds since 1970) when the token expires.
+ public let expiresAt: Int64
+
+ /// Initializes a new instance of `Token`.
+ /// - Parameters:
+ /// - accessToken: The access token string.
+ /// - tokenType: The type of token.
+ /// - scope: The scope of access granted by the token.
+ /// - expiresIn: The duration (in seconds) for which the token is valid.
+ /// - refreshToken: The refresh token string (optional).
+ /// - idToken: The ID token string (optional).
+ public init(accessToken: String, tokenType: String?, scope: String?, expiresIn: Int64, refreshToken: String?, idToken: String?) {
+ self.accessToken = accessToken
+ self.tokenType = tokenType
+ self.scope = scope
+ self.expiresIn = expiresIn
+ self.refreshToken = refreshToken
+ self.idToken = idToken
+ self.expiresAt = Int64(Date().timeIntervalSince1970) + expiresIn
+ }
+
+ /// A Boolean value indicating whether the token has expired.
+ /// - Returns: `true` if the current time is greater than or equal to the token's expiry time; otherwise, `false`.
+ public var isExpired: Bool {
+ return Int64(Date().timeIntervalSince1970) >= expiresAt
+ }
+
+ /// Checks if the token will expire within a specified threshold.
+ /// - Parameter threshold: The threshold duration (in seconds) to check for expiration.
+ /// - Returns: `true` if the token will expire within the threshold; otherwise, `false`.
+ public func isExpired(threshold: Int64) -> Bool {
+ return Int64(Date().timeIntervalSince1970) >= expiresAt - threshold
+ }
+
+ /// Decodes a `Token` instance from a decoder.
+ /// - Parameter decoder: The decoder instance used for decoding.
+ /// - Throws: An error if decoding fails.
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ accessToken = try container.decode(String.self, forKey: .accessToken)
+ tokenType = try container.decodeIfPresent(String.self, forKey: .tokenType)
+ scope = try container.decodeIfPresent(String.self, forKey: .scope)
+ expiresIn = try container.decode(Int64.self, forKey: .expiresIn)
+ refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken)
+ idToken = try container.decodeIfPresent(String.self, forKey: .idToken)
+ expiresAt = try container.decodeIfPresent(Int64.self, forKey: .expiresAt) ?? Int64(Date().timeIntervalSince1970) + expiresIn
+ }
+
+ /// Encodes the `Token` instance to an encoder.
+ /// - Parameter encoder: The encoder instance used for encoding.
+ /// - Throws: An error if encoding fails.
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(accessToken, forKey: .accessToken)
+ try container.encode(tokenType, forKey: .tokenType)
+ try container.encode(scope, forKey: .scope)
+ try container.encode(expiresIn, forKey: .expiresIn)
+ try container.encode(refreshToken, forKey: .refreshToken)
+ try container.encode(idToken, forKey: .idToken)
+ try container.encode(expiresAt, forKey: .expiresAt)
+ }
+}
+
+
+/// Define CodingKeys for the AccessToken struct
+extension Token {
+ /// Coding keys used for encoding and decoding the `Token` struct.
+ enum CodingKeys: String, CodingKey {
+ case accessToken = "access_token"
+ case tokenType = "token_type"
+ case scope
+ case expiresIn = "expires_in"
+ case refreshToken = "refresh_token"
+ case idToken = "id_token"
+ case expiresAt = "expires_at"
+ }
+}
+
+
+extension Token: CustomStringConvertible {
+ /// A textual representation of the `Token` instance.
+ public var description: String {
+ "isExpired: \(isExpired)\n access_token: \(self.accessToken)\n refresh_token: \(refreshToken ?? "nil")\n id_token: \(idToken ?? "nil")\n token_type: \(tokenType ?? "nil")\n scope: \(scope ?? "nil")\n expires_in: \(String(describing: expiresIn))\n expires_at: \(String(describing: Date(timeIntervalSince1970: TimeInterval(expiresAt))))"
+ }
+}
diff --git a/Oidc/Oidc/User.swift b/Oidc/Oidc/User.swift
new file mode 100644
index 0000000..4d3fcdf
--- /dev/null
+++ b/Oidc/Oidc/User.swift
@@ -0,0 +1,33 @@
+//
+// User.swift
+// PingOidc
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+/// Protocol for a User.
+/// Provides methods for token management, user information retrieval, and logout.
+public protocol User {
+ /// Retrieves the token for the user.
+ /// - Returns: A `Result` object containing either the `Token` or an `OidcError`.
+ func token() async -> Result
+
+ /// Revokes the user's token.
+ func revoke() async
+
+ /// Retrieves the user's information.
+ /// - Parameter cache: Whether to cache the user information.
+ /// - Returns: A `Result` object containing either the user information as a `UserInfo` or an `OidcError`.
+ func userinfo(cache: Bool) async -> Result
+
+ /// Logs out the user.
+ func logout() async
+}
+
+
+/// A type alias representing user information as a dictionary.
+public typealias UserInfo = [String: Any]
diff --git a/Oidc/OidcTests/OidcClientConfigTests.swift b/Oidc/OidcTests/OidcClientConfigTests.swift
new file mode 100644
index 0000000..3fc054a
--- /dev/null
+++ b/Oidc/OidcTests/OidcClientConfigTests.swift
@@ -0,0 +1,208 @@
+//
+// OidcClientConfigTests.swift
+// OidcTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingOidc
+@testable import PingOrchestrate
+@testable import PingLogger
+@testable import PingStorage
+
+final class OidcClientConfigTests: XCTestCase {
+
+ var oidcClientConfig: OidcClientConfig!
+
+ override func setUp() {
+ super.setUp()
+ oidcClientConfig = OidcClientConfig()
+ oidcClientConfig.discoveryEndpoint = MockAPIEndpoint.discovery.url.absoluteString
+ oidcClientConfig.storage = MockStorage()
+ oidcClientConfig.httpClient = HttpClient(session: .shared)
+ MockURLProtocol.startInterceptingRequests()
+ }
+
+ override func tearDown() {
+ oidcClientConfig = nil
+ MockURLProtocol.stopInterceptingRequests()
+ super.tearDown()
+ }
+
+ // TestRailCase(22106)
+ func testDefaultInitialization() {
+ oidcClientConfig = OidcClientConfig()
+
+ XCTAssertNil(oidcClientConfig.openId)
+ XCTAssertEqual(oidcClientConfig.refreshThreshold, 0)
+ XCTAssertNil(oidcClientConfig.agent)
+ XCTAssertEqual(oidcClientConfig.discoveryEndpoint, "")
+ XCTAssertEqual(oidcClientConfig.clientId, "")
+ XCTAssertTrue(oidcClientConfig.scopes.isEmpty)
+ XCTAssertEqual(oidcClientConfig.redirectUri, "")
+ XCTAssertNil(oidcClientConfig.loginHint)
+ XCTAssertNil(oidcClientConfig.state)
+ XCTAssertNil(oidcClientConfig.nonce)
+ XCTAssertNil(oidcClientConfig.display)
+ XCTAssertNil(oidcClientConfig.prompt)
+ XCTAssertNil(oidcClientConfig.uiLocales)
+ XCTAssertNil(oidcClientConfig.acrValues)
+ XCTAssertTrue(oidcClientConfig.additionalParameters.isEmpty)
+ XCTAssertNil(oidcClientConfig.httpClient)
+ }
+
+ func testUpdateAgent() {
+ let agent = MockAgent()
+ oidcClientConfig.updateAgent(agent)
+ XCTAssertNotNil(oidcClientConfig.agent)
+ }
+
+ // TestRailCase(22118)
+ func testScopeInsertion() {
+ oidcClientConfig.scope("openid")
+ XCTAssertTrue(oidcClientConfig.scopes.contains("openid"))
+ }
+
+ // TestRailCase(22118)
+ func testOidcInitializeInvalidDiscovery() async throws {
+
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.error)
+ }
+
+ do {
+ try await oidcClientConfig.oidcInitialize()
+ } catch {
+ XCTAssertNotNil(error)
+ }
+ XCTAssertNil(oidcClientConfig.openId)
+ }
+
+ // TestRailCase(24720)
+ func testOidcInitializeValidDiscovery() async throws {
+
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration)
+ }
+
+ do {
+ try await oidcClientConfig.oidcInitialize()
+ XCTAssertNotNil(oidcClientConfig.openId)
+ XCTAssertEqual(MockAPIEndpoint.authorization.url.absoluteString, oidcClientConfig.openId!.authorizationEndpoint)
+ XCTAssertEqual(MockAPIEndpoint.token.url.absoluteString, oidcClientConfig.openId!.tokenEndpoint)
+ XCTAssertEqual(MockAPIEndpoint.userinfo.url.absoluteString, oidcClientConfig.openId!.userinfoEndpoint)
+ XCTAssertEqual(MockAPIEndpoint.endSession.url.absoluteString, oidcClientConfig.openId!.endSessionEndpoint)
+ XCTAssertEqual(MockAPIEndpoint.revocation.url.absoluteString, oidcClientConfig.openId!.revocationEndpoint)
+ } catch {
+ XCTFail("Initialization failed with error: \(error)")
+ }
+ }
+
+ // TestRailCase(22081)
+ func testClone() {
+ oidcClientConfig.refreshThreshold = 100
+ oidcClientConfig.agent = AgentDelegate(agent: MockAgent(), agentConfig: (), oidcClientConfig: oidcClientConfig)
+ oidcClientConfig.logger = LogManager.standard
+ oidcClientConfig.storage = MockStorage()
+ oidcClientConfig.discoveryEndpoint = "https://example.com"
+ oidcClientConfig.clientId = "clientId"
+ oidcClientConfig.scopes.insert("openid")
+ oidcClientConfig.redirectUri = "http://localhost/callback"
+ oidcClientConfig.loginHint = "loginHint"
+ oidcClientConfig.nonce = "nonce"
+ oidcClientConfig.display = "display"
+ oidcClientConfig.prompt = "prompt"
+ oidcClientConfig.uiLocales = "uiLocales"
+ oidcClientConfig.acrValues = "acrValues"
+ oidcClientConfig.additionalParameters = ["param": "value"]
+ oidcClientConfig.httpClient = HttpClient()
+
+ let clonedConfig = oidcClientConfig.clone()
+
+ XCTAssertEqual(oidcClientConfig.openId.debugDescription, clonedConfig.openId.debugDescription)
+ XCTAssertEqual(oidcClientConfig.refreshThreshold, clonedConfig.refreshThreshold)
+ XCTAssertEqual(oidcClientConfig.agent.debugDescription, clonedConfig.agent.debugDescription)
+ XCTAssertEqual(oidcClientConfig.discoveryEndpoint, clonedConfig.discoveryEndpoint)
+ XCTAssertEqual(oidcClientConfig.clientId, clonedConfig.clientId)
+ XCTAssertEqual(oidcClientConfig.scopes, clonedConfig.scopes)
+ XCTAssertEqual(oidcClientConfig.redirectUri, clonedConfig.redirectUri)
+ XCTAssertEqual(oidcClientConfig.loginHint, clonedConfig.loginHint)
+ XCTAssertEqual(oidcClientConfig.nonce, clonedConfig.nonce)
+ XCTAssertEqual(oidcClientConfig.display, clonedConfig.display)
+ XCTAssertEqual(oidcClientConfig.prompt, clonedConfig.prompt)
+ XCTAssertEqual(oidcClientConfig.uiLocales, clonedConfig.uiLocales)
+ XCTAssertEqual(oidcClientConfig.acrValues, clonedConfig.acrValues)
+ XCTAssertEqual(oidcClientConfig.additionalParameters, clonedConfig.additionalParameters)
+ XCTAssertEqual(oidcClientConfig.httpClient.debugDescription, clonedConfig.httpClient.debugDescription)
+ }
+
+ // TestRailCase(24719)
+ func testUpdate() {
+ let otherConfig = OidcClientConfig()
+ otherConfig.agent = AgentDelegate(agent: MockAgent(), agentConfig: (), oidcClientConfig: oidcClientConfig)
+ otherConfig.logger = LogManager.standard
+ otherConfig.storage = MockStorage()
+ otherConfig.discoveryEndpoint = "https://example.com"
+ otherConfig.clientId = "clientId"
+ otherConfig.scopes.insert("openid")
+ otherConfig.redirectUri = "http://localhost/callback"
+ otherConfig.loginHint = "loginHint"
+ otherConfig.nonce = "nonce"
+ otherConfig.display = "display"
+ otherConfig.prompt = "prompt"
+ otherConfig.uiLocales = "uiLocales"
+ otherConfig.acrValues = "acrValues"
+ otherConfig.additionalParameters = ["param": "value"]
+ otherConfig.httpClient = HttpClient()
+
+ oidcClientConfig.update(with: otherConfig)
+
+ XCTAssertEqual(otherConfig.openId.debugDescription, oidcClientConfig.openId.debugDescription)
+ XCTAssertEqual(otherConfig.agent.debugDescription, oidcClientConfig.agent.debugDescription)
+ XCTAssertEqual(otherConfig.discoveryEndpoint, oidcClientConfig.discoveryEndpoint)
+ XCTAssertEqual(otherConfig.clientId, oidcClientConfig.clientId)
+ XCTAssertEqual(otherConfig.scopes, oidcClientConfig.scopes)
+ XCTAssertEqual(otherConfig.redirectUri, oidcClientConfig.redirectUri)
+ XCTAssertEqual(otherConfig.loginHint, oidcClientConfig.loginHint)
+ XCTAssertEqual(otherConfig.nonce, oidcClientConfig.nonce)
+ XCTAssertEqual(otherConfig.display, oidcClientConfig.display)
+ XCTAssertEqual(otherConfig.prompt, oidcClientConfig.prompt)
+ XCTAssertEqual(otherConfig.uiLocales, oidcClientConfig.uiLocales)
+ XCTAssertEqual(otherConfig.acrValues, oidcClientConfig.acrValues)
+ XCTAssertEqual(otherConfig.additionalParameters, oidcClientConfig.additionalParameters)
+ XCTAssertEqual(otherConfig.httpClient.debugDescription, oidcClientConfig.httpClient.debugDescription)
+ }
+}
+
+// Mock classes for AgentDelegateProtocol, Agent, HttpClient, etc.
+class MockAgent: Agent {
+ func config() -> () -> T {
+ return {}
+ }
+
+ func endSession(oidcConfig: PingOidc.OidcConfig, idToken: String) async throws -> Bool {
+ let params = [
+ "client_id": oidcConfig.oidcClientConfig.clientId,
+ "id_token_hint": idToken
+ ]
+ let request = Request()
+ request.url(MockAPIEndpoint.endSession.url.absoluteString)
+ request.form(formData: params)
+ do {
+ let (_, _) = try await oidcConfig.oidcClientConfig.httpClient!.sendRequest(request: request)
+ } catch {
+ }
+ return true
+ }
+
+ func authorize(oidcConfig: PingOidc.OidcConfig) async throws -> PingOidc.AuthCode {
+ return AuthCode(code: "TestAgent", codeVerifier: "codeVerifier")
+ }
+
+ typealias T = Void
+}
diff --git a/Oidc/OidcTests/OidcClientTests.swift b/Oidc/OidcTests/OidcClientTests.swift
new file mode 100644
index 0000000..a5a0006
--- /dev/null
+++ b/Oidc/OidcTests/OidcClientTests.swift
@@ -0,0 +1,319 @@
+//
+// OidcClientTests.swift
+// OidcTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingOidc
+@testable import PingOrchestrate
+@testable import PingStorage
+
+final class OidcClientTests: XCTestCase {
+ var oidcClientConfig: OidcClientConfig!
+ var oidcClient: OidcClient!
+
+ override func setUp() {
+ super.setUp()
+ oidcClientConfig = OidcClientConfig()
+ let agent = MockAgent()
+ oidcClientConfig.updateAgent(agent)
+ oidcClientConfig.discoveryEndpoint = MockAPIEndpoint.discovery.url.absoluteString
+ oidcClientConfig.storage = MockStorage()
+ oidcClientConfig.clientId = "test-client-id"
+ oidcClientConfig.httpClient = HttpClient(session: .shared)
+ oidcClient = OidcClient(config: oidcClientConfig)
+
+ MockURLProtocol.startInterceptingRequests()
+
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration)
+ case MockAPIEndpoint.token.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.token)
+ case MockAPIEndpoint.userinfo.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.userinfo.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.userinfo)
+ case MockAPIEndpoint.revocation.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.revocation.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, Data())
+ case MockAPIEndpoint.endSession.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.endSession.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, Data())
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+ }
+
+ override func tearDown() {
+ MockURLProtocol.stopInterceptingRequests()
+
+ oidcClient = nil
+ oidcClientConfig = nil
+ super.tearDown()
+ }
+
+ // TestRailCase(22118)
+ func testFailedToLookupDiscoveryEndpoint() async throws {
+
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: MockResponse.headers)!, Data())
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let result = await oidcClient.token()
+
+ switch result {
+ case .success(_):
+ XCTFail("Should have failed with error")
+ case .failure(let failure):
+ switch failure {
+ case .apiError(let code, _):
+ XCTAssertEqual(code, 500)
+ case .authorizeError, .networkError, .unknown:
+ XCTFail("Should have failed with .apiError")
+ }
+ }
+ }
+
+ // TestRailCase(22085)
+ func testAccessTokenShouldReturnCachedTokenIfNotExpired() async throws {
+ let result = await oidcClient.token()
+ switch result {
+ case .success( _):
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+ let cached = await oidcClient.token()
+ switch cached {
+ case .success( let token):
+ XCTAssertEqual(token.accessToken, "Dummy AccessToken")
+ XCTAssertEqual(token.tokenType, "Dummy Token Type")
+ XCTAssertEqual(token.refreshToken, "Dummy RefreshToken")
+ XCTAssertEqual(token.idToken, "Dummy IdToken")
+ XCTAssertEqual(token.scope, "openid email address")
+ break
+ case .failure(let error):
+ XCTFail("Should have succeeded, but failed with error \(error.errorMessage)")
+ }
+
+ XCTAssertEqual(MockURLProtocol.requestHistory.count, 2)
+ }
+
+ // TestRailCase(22086)
+ func testAccessTokenShouldRefreshTokenIfExpired() async throws {
+ let result = await oidcClient.token()
+ switch result {
+ case .success( _):
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ // Advance time by 1 second
+ try await Task.sleep(nanoseconds: 2_000_000_000)
+
+ _ = await oidcClient.token()
+
+ // auto refresh has been triggered
+ XCTAssertEqual(MockURLProtocol.requestHistory.count, 3)
+ XCTAssertEqual(Int(MockURLProtocol.requestHistory.last!.value(forHTTPHeaderField: "Content-Length")!), "grant_type=refresh_token&refresh_token=Dummy RefreshToken&client_id=test-client-id".count)
+ }
+
+ // TestRailCase(24712)
+ func testRevokeShouldDeleteTokenFromStorage() async throws {
+ // First, get an access token
+ let result = await oidcClient.token()
+ switch result {
+ case .success( _):
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ // Then, revoke the access token
+ await oidcClient.revoke()
+
+ // Check that the token is no longer in storage
+ let tokenInStorage = try await oidcClientConfig.storage.get()
+ XCTAssertNil(tokenInStorage)
+ }
+
+ // TestRailCase(22087)
+ func testUserinfoShouldReturnUserInfo() async throws {
+ let result = await oidcClient.userinfo()
+ switch result {
+ case .success(let userinfo):
+ XCTAssertEqual(userinfo["sub"] as? String, "test-sub")
+ XCTAssertEqual(userinfo["name"] as? String, "test-name")
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+ }
+
+ // TestRailCase(22088)
+ func testEndSessionShouldEndSessionAndRevokeToken() async throws {
+ // First, get an access token
+ let result = await oidcClient.token()
+ switch result {
+ case .success( _):
+ break
+ case .failure(_):
+ XCTFail("Should have succeeded")
+ }
+
+ // Then, end the session
+ let endSessionResult = await oidcClient.endSession()
+ XCTAssertTrue(endSessionResult)
+
+ // Check that the token is no longer in storage
+ let tokenInStorage = try await oidcClientConfig.storage.get()
+ XCTAssertNil(tokenInStorage)
+
+ let revokeCalled = MockURLProtocol.requestHistory.contains(where: { request in
+ request.url?.path == MockAPIEndpoint.revocation.url.path
+ })
+
+ XCTAssertTrue(revokeCalled, "The /revoke endpoint was not called.")
+
+ let signOffCalled = MockURLProtocol.requestHistory.contains(where: { request in
+ request.url?.path == MockAPIEndpoint.endSession.url.path
+ })
+ XCTAssertTrue(signOffCalled, "The /signoff endpoint was not called.")
+ }
+
+ // TestRailCase(22091)
+ func testFailedToRetrieveAccessToken() async throws {
+
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration)
+ case MockAPIEndpoint.token.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenErrorResponse)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let result = await oidcClient.token()
+ switch result {
+ case .success(_):
+ XCTFail("Should have failed with error")
+ case .failure(let failure):
+ switch failure {
+ case .apiError(let code, _):
+ XCTAssertEqual(code, 400)
+ case .authorizeError, .networkError, .unknown:
+ XCTFail("Should have failed with .apiError(400)")
+ }
+ }
+ }
+
+ // TestRailCase(22092)
+ func testFailedToInjectAccessTokenToUserinfo() async throws {
+
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration)
+ case MockAPIEndpoint.token.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenErrorResponse)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let result = await oidcClient.userinfo()
+ switch result {
+ case .success(_):
+ XCTFail("Should have failed with error")
+ case .failure(let failure):
+ switch failure {
+ case .apiError(let code, _):
+ XCTAssertEqual(code, 400)
+ case .authorizeError, .networkError, .unknown:
+ XCTFail("Should have failed with .apiError(400)")
+ }
+ }
+ }
+
+ // TestRailCase(22093)
+ func testFailedToRetrieveUserinfo() async throws {
+
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration)
+ case MockAPIEndpoint.token.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.token)
+ case MockAPIEndpoint.userinfo.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.userinfo.url, statusCode: 401, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenErrorResponse)
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let result = await oidcClient.userinfo()
+ switch result {
+ case .success(_):
+ XCTFail("Should have failed with error")
+ case .failure(let failure):
+ switch failure {
+ case .apiError(let code, _):
+ XCTAssertEqual(code, 401)
+ case .authorizeError, .networkError, .unknown:
+ XCTFail("Should have failed with .apiError(401)")
+ }
+ }
+ }
+
+ // TestRailCase(22094)
+ func testFailedToRefreshTokenAfterTokenExpired() async throws {
+
+ MockURLProtocol.requestHandler = { request in
+ switch request.url!.path {
+ case MockAPIEndpoint.discovery.url.path:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.openIdConfiguration)
+ case MockAPIEndpoint.token.url.path:
+ // as httpBody is not available her (it is nil), we will check the `Content-Length` header value to see if the `grant_type` is `refresh_token'
+ if Int(request.value(forHTTPHeaderField: "Content-Length")!) == "grant_type=refresh_token&refresh_token=Dummy RefreshToken&client_id=test-client-id".count {
+ return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 400, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.tokenErrorResponse)
+ } else {
+ return (HTTPURLResponse(url: MockAPIEndpoint.token.url, statusCode: 200, httpVersion: nil, headerFields: MockResponse.headers)!, MockResponse.token)
+ }
+ default:
+ return (HTTPURLResponse(url: MockAPIEndpoint.discovery.url, statusCode: 500, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ }
+
+ let result = await oidcClient.token()
+ switch result {
+ case .success( _): break
+ case .failure( _): XCTFail("Should have succeeded")
+ }
+
+ try await Task.sleep(nanoseconds: 2_000_000_000)
+
+ let refreshResult = await oidcClient.token()
+ switch refreshResult {
+ case .success( _): break
+ case .failure( _): XCTFail("Should have succeeded")
+ }
+
+ let revokeCalled = MockURLProtocol.requestHistory.contains(where: { request in
+ request.url?.path == "/revoke"
+ })
+
+ XCTAssertTrue(revokeCalled, "The /revoke endpoint was not called.")
+ }
+}
diff --git a/Oidc/OidcTests/PkceTests.swift b/Oidc/OidcTests/PkceTests.swift
new file mode 100644
index 0000000..1bc6821
--- /dev/null
+++ b/Oidc/OidcTests/PkceTests.swift
@@ -0,0 +1,34 @@
+//
+// PkceTests.swift
+// OidcTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingOidc
+
+final class PkceTests: XCTestCase {
+
+ // TestRailCase(22111)
+ func testGeneratePkce() {
+ let pkce = Pkce.generate()
+
+ XCTAssertFalse(pkce.codeVerifier.isEmpty, "Code verifier should not be empty")
+ XCTAssertFalse(pkce.codeChallenge.isEmpty, "Code challenge should not be empty")
+ XCTAssertEqual(pkce.codeChallengeMethod, "S256", "Code challenge method should be 'S256'")
+ }
+
+ // TestRailCase(22110)
+ func testGenerateDifferentPkce() {
+ let pkce1 = Pkce.generate()
+ let pkce2 = Pkce.generate()
+
+ XCTAssertTrue(pkce1.codeVerifier != pkce2.codeVerifier, "Code verifier should be different")
+ XCTAssertTrue(pkce1.codeChallenge != pkce2.codeChallenge, "Code challenge should be different")
+ }
+}
diff --git a/Oidc/OidcTests/TokenTests.swift b/Oidc/OidcTests/TokenTests.swift
new file mode 100644
index 0000000..c7696af
--- /dev/null
+++ b/Oidc/OidcTests/TokenTests.swift
@@ -0,0 +1,90 @@
+//
+// TokenTests.swift
+// OidcTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingOidc
+
+final class TokenTests: XCTestCase {
+
+ func testInitialization() {
+ let token = Token(
+ accessToken: "testAccessToken",
+ tokenType: "Bearer",
+ scope: "testScope",
+ expiresIn: 3600,
+ refreshToken: "testRefreshToken",
+ idToken: "testIdToken"
+ )
+
+ XCTAssertEqual(token.accessToken, "testAccessToken")
+ XCTAssertEqual(token.tokenType, "Bearer")
+ XCTAssertEqual(token.scope, "testScope")
+ XCTAssertEqual(token.expiresIn, 3600)
+ XCTAssertEqual(token.refreshToken, "testRefreshToken")
+ XCTAssertEqual(token.idToken, "testIdToken")
+ XCTAssertFalse(token.isExpired)
+ }
+
+ // TestRailCase(22116, 22117)
+ func testEncodingDecoding() throws {
+ let token = Token(
+ accessToken: "testAccessToken",
+ tokenType: "Bearer",
+ scope: "testScope",
+ expiresIn: 3600,
+ refreshToken: "testRefreshToken",
+ idToken: "testIdToken"
+ )
+
+ let encoder = JSONEncoder()
+ let decoder = JSONDecoder()
+
+ let data = try encoder.encode(token)
+ let decodedToken = try decoder.decode(Token.self, from: data)
+
+ XCTAssertEqual(decodedToken.accessToken, "testAccessToken")
+ XCTAssertEqual(decodedToken.tokenType, "Bearer")
+ XCTAssertEqual(decodedToken.scope, "testScope")
+ XCTAssertEqual(decodedToken.expiresIn, 3600)
+ XCTAssertEqual(decodedToken.refreshToken, "testRefreshToken")
+ XCTAssertEqual(decodedToken.idToken, "testIdToken")
+ XCTAssertEqual(decodedToken.expiresAt, token.expiresAt)
+ }
+
+ // TestRailCase(22112)
+ func testIsExpired() {
+ let token = Token(
+ accessToken: "testAccessToken",
+ tokenType: "Bearer",
+ scope: "testScope",
+ expiresIn: -1,
+ refreshToken: "testRefreshToken",
+ idToken: "testIdToken"
+ )
+
+ XCTAssertTrue(token.isExpired)
+ }
+
+ // TestRailCase(22114, 22115)
+ func testIsExpiredWithThreshold() {
+ let token = Token(
+ accessToken: "testAccessToken",
+ tokenType: "Bearer",
+ scope: "testScope",
+ expiresIn: 3600,
+ refreshToken: "testRefreshToken",
+ idToken: "testIdToken"
+ )
+
+ XCTAssertTrue(token.isExpired(threshold: 3601))
+ XCTAssertFalse(token.isExpired(threshold: 3599))
+ }
+}
diff --git a/Oidc/OidcTests/mock/MockAPIEndpoint.swift b/Oidc/OidcTests/mock/MockAPIEndpoint.swift
new file mode 100644
index 0000000..55cd52c
--- /dev/null
+++ b/Oidc/OidcTests/mock/MockAPIEndpoint.swift
@@ -0,0 +1,40 @@
+//
+// MockAPIEndpoint.swift
+// OidcTests
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+enum MockAPIEndpoint {
+ static let baseURL = "https://auth.test-one-pingone.com"
+
+ case authorization
+ case token
+ case userinfo
+ case endSession
+ case revocation
+ case discovery
+
+ var url: URL {
+ switch self {
+ case .authorization:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/authorize")!
+ case .token:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/token")!
+ case .userinfo:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/userinfo")!
+ case .endSession:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/signoff")!
+ case .revocation:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/revoke")!
+ case .discovery:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/.well-known/openid-configuration")!
+ }
+ }
+}
diff --git a/Oidc/OidcTests/mock/MockResponse.swift b/Oidc/OidcTests/mock/MockResponse.swift
new file mode 100644
index 0000000..dd13dce
--- /dev/null
+++ b/Oidc/OidcTests/mock/MockResponse.swift
@@ -0,0 +1,69 @@
+//
+// MockResponse.swift
+// OidcTests
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+struct MockResponse {
+ static let headers = ["Content-Type": "application/json"]
+
+ static var openIdConfiguration: Data {
+ """
+ {
+ "authorization_endpoint" : "\(MockAPIEndpoint.authorization.url.absoluteString)",
+ "token_endpoint" : "\(MockAPIEndpoint.token.url.absoluteString)",
+ "userinfo_endpoint" : "\(MockAPIEndpoint.userinfo.url.absoluteString)",
+ "end_session_endpoint" : "\(MockAPIEndpoint.endSession.url.absoluteString)",
+ "revocation_endpoint" : "\(MockAPIEndpoint.revocation.url.absoluteString)"
+ }
+ """.data(using: .utf8)!
+ }
+
+ static var token: Data {
+ """
+ {
+ "access_token" : "Dummy AccessToken",
+ "token_type" : "Dummy Token Type",
+ "scope" : "openid email address",
+ "refresh_token" : "Dummy RefreshToken",
+ "expires_in" : 2,
+ "id_token" : "Dummy IdToken"
+ }
+ """.data(using: .utf8)!
+ }
+
+ static var userinfo: Data {
+ """
+ {
+ "sub" : "test-sub",
+ "name" : "test-name",
+ "email" : "test-email",
+ "phone_number" : "test-phone_number",
+ "address" : "test-address"
+ }
+ """.data(using: .utf8)!
+ }
+
+ static var tokenErrorResponse: Data {
+ """
+ {
+ "error" : "Invalid Grant"
+ }
+ """.data(using: .utf8)!
+ }
+
+ static var error: Data {
+ """
+ {
+ "error" : "Internal Server Error"
+ }
+ """.data(using: .utf8)!
+ }
+}
diff --git a/Oidc/OidcTests/mock/MockStorage.swift b/Oidc/OidcTests/mock/MockStorage.swift
new file mode 100644
index 0000000..9fb6eee
--- /dev/null
+++ b/Oidc/OidcTests/mock/MockStorage.swift
@@ -0,0 +1,36 @@
+//
+// MockStorage.swift
+// OidcTests
+//
+// Copyright (c) 2024 Ping Identity. 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 PingStorage
+
+public class Mock: Storage {
+ private var data: T?
+
+ public func save(item: T) async throws {
+ data = item
+ }
+
+ public func get() async throws -> T? {
+ return data
+ }
+
+ public func delete() async throws {
+ data = nil
+ }
+}
+
+public class MockStorage: StorageDelegate {
+ public init(cacheable: Bool = false) {
+ super.init(delegate: Mock(), cacheable: cacheable)
+ }
+}
+
diff --git a/Oidc/OidcTests/mock/MockURLProtocol.swift b/Oidc/OidcTests/mock/MockURLProtocol.swift
new file mode 100644
index 0000000..984cfa4
--- /dev/null
+++ b/Oidc/OidcTests/mock/MockURLProtocol.swift
@@ -0,0 +1,58 @@
+//
+// MockURLProtocol.swift
+// OidcTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+import PingLogger
+
+class MockURLProtocol: URLProtocol {
+ public static var requestHistory: [URLRequest] = [URLRequest]()
+
+ static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
+
+ static func startInterceptingRequests() {
+ URLProtocol.registerClass(MockURLProtocol.self)
+ }
+
+ static func stopInterceptingRequests() {
+ URLProtocol.unregisterClass(MockURLProtocol.self)
+ requestHistory.removeAll()
+ }
+
+ override class func canInit(with request: URLRequest) -> Bool {
+ return true
+ }
+
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+ return request
+ }
+
+ override func startLoading() {
+ MockURLProtocol.requestHistory.append(request)
+
+ guard let handler = MockURLProtocol.requestHandler else {
+ XCTFail("Received unexpected request with no handler set")
+ return
+ }
+ do {
+ let (response, data) = try handler(request)
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ client?.urlProtocol(self, didLoad: data)
+ client?.urlProtocolDidFinishLoading(self)
+ } catch {
+ client?.urlProtocol(self, didFailWithError: error)
+ }
+ }
+
+ override func stopLoading() {
+
+ }
+}
diff --git a/Oidc/README.md b/Oidc/README.md
new file mode 100644
index 0000000..4f12c92
--- /dev/null
+++ b/Oidc/README.md
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+`PingOidc` module provides OIDC client for PingOne and ForgeRock platform.
+
+The `PingOidc` module follows the [OIDC](https://openid.net/specs/openid-connect-core-1_0.html) specification and
+provides a simple and easy-to-use API to interact with the OIDC server. It allows you to authenticate, retrieve the
+access token, revoke the token, and sign out from the OIDC server.
+
+## Integrating the SDK into your project
+
+Use Cocoapods or Swift Package Manger
+
+## Oidc Client Configuration
+
+Basic Configuration, use `discoveryEndpoint` to lookup OIDC endpoints
+
+```swift
+let config = OidcClientConfig()
+config.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration"
+config.clientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1"
+config.redirectUri = "org.forgerock.demo://oauth2redirect"
+config.scopes = ["openid", "email", "address", "profile", "phone"]
+
+let ping = OidcClient(config: config)
+
+let result = await ping.token() // Retrieve the access token
+switch result {
+case .success(let token):
+ let accessToken = token
+case .failure(let error):
+ switch error {
+ case .apiError:
+ //Address error
+ break
+ case .authorizeError:
+ //Address error
+ break
+ case .networkError:
+ //Address error
+ break
+ case .unknown:
+ //Address error
+ break
+ }
+}
+
+await ping.revoke() //Revoke the access token
+_ = await ping.endSession() //End the session
+```
+
+By default, the SDK uses `KeychainStorage` (with `SecuredKeyEncryptor` ) to store the token and `none` Logger is set,
+however developers can override the storage and logger settings.
+
+Basic Configuration with custom `storage` and `logger`
+
+```swift
+let config = OidcClientConfig()
+config.logger = LogManager.standard //Log to console
+config.storage = CustomStorage() //Use Custom Storage
+//...
+
+let ping = OidcClient(config: config)
+```
+
+More OidcClient configuration, configurable attribute can be found under
+[OIDC Spec](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest)
+
+```swift
+let config = OidcClientConfig()
+config.acrValues = "urn:acr:form"
+config.loginHint = "test"
+config.display = "test"
+//...
+
+let ping = OidcClient(config: config)
+```
+
+## Custom Agent
+
+You can also provide a custom agent to launch the authorization request.
+You can implement the `Agent` interface to create a custom agent.
+
+```swift
+protocol Agent {
+ associatedtype T
+
+ func config() -> () -> T
+ func endSession(oidcConfig: OidcConfig, idToken: String) async throws -> Bool
+ func authorize(oidcConfig: OidcConfig) async throws -> AuthCode
+}
+```
+
+Here is an example of creating a custom agent.
+
+```swift
+//Create a custom agent configuration
+struct CustomAgentConfig {
+ var config1 = "config1Value"
+ var config2 = "config2Value"
+}
+
+class CustomAgent: Agent {
+ func config() -> () -> CustomAgentConfig {
+ return { CustomAgentConfig() }
+ }
+
+ func authorize(oidcConfig: Oidc.OidcConfig) async throws -> Oidc.AuthCode {
+ oidcConfig.config.config2 //Access the agent configuration
+ oidcConfig.oidcClientConfig.openId?.endSessionEndpoint //Access the oidcClientConfig
+ return AuthCode(code: "TestAgent", codeVerifier: "")
+ }
+
+ func endSession(oidcConfig: Oidc.OidcConfig, idToken: String) async throws -> Bool {
+ //Logout session with idToken
+ oidcConfig.config.config1 //Access the agent configuration
+ oidcConfig.oidcClientConfig.openId?.endSessionEndpoint //Access the oidcClientConfig
+ return true
+ }
+}
+
+let config = OidcClientConfig()
+config.updateAgent(CustomAgent())
+//...
+
+let ping = OidcClient(config: config)
+
+```
diff --git a/Orchestrate/Orchestrate.xcodeproj/project.pbxproj b/Orchestrate/Orchestrate.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..bfa9e32
--- /dev/null
+++ b/Orchestrate/Orchestrate.xcodeproj/project.pbxproj
@@ -0,0 +1,628 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 63;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 3A12F9BE2BCE20B50087DF67 /* Workflow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A12F9BD2BCE20B50087DF67 /* Workflow.swift */; };
+ 3A203D592BD9DC600020C995 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D582BD9DC600020C995 /* Request.swift */; };
+ 3A203D5B2BD9E2C80020C995 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203D5A2BD9E2C80020C995 /* Node.swift */; };
+ 3A203D772BDA1CEC0020C995 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 3A203D762BDA1CEC0020C995 /* README.md */; };
+ 3A203DA22BE0312A0020C995 /* HttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA12BE0312A0020C995 /* HttpClient.swift */; };
+ 3A203DA42BE068BE0020C995 /* WorkflowConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA32BE068BE0020C995 /* WorkflowConfig.swift */; };
+ 3A203DA62BE068E10020C995 /* ModuleRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA52BE068E10020C995 /* ModuleRegistry.swift */; };
+ 3A203DA82BE069120020C995 /* Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA72BE069120020C995 /* Setup.swift */; };
+ 3A203DAA2BE06DFF0020C995 /* CookieModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A203DA92BE06DFF0020C995 /* CookieModule.swift */; };
+ 3A2E86902C179E2F000EC6E2 /* FlowContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2E868F2C179E2F000EC6E2 /* FlowContextTests.swift */; };
+ 3A54417F2BCDF1D900385131 /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */; };
+ 3A5441842BCDF1D900385131 /* OrchestrateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441832BCDF1D900385131 /* OrchestrateTests.swift */; };
+ 3A5441852BCDF1D900385131 /* Orchestrate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A5441772BCDF1D900385131 /* Orchestrate.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 3A7575762C063F2A00891EC7 /* ModuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575752C063F2A00891EC7 /* ModuleTests.swift */; };
+ 3A7575782C0673A100891EC7 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575772C0673A100891EC7 /* ResponseTests.swift */; };
+ 3A75757A2C06947000891EC7 /* SharedContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7575792C06947000891EC7 /* SharedContext.swift */; };
+ 3A75757C2C06B59F00891EC7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A75757B2C06B59F00891EC7 /* RequestTests.swift */; };
+ 3AB1C9E92BD6410A003FCE3C /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1C9E52BD6410A003FCE3C /* Response.swift */; };
+ 3AB1C9F22BD6B2F9003FCE3C /* CustomHTTPCookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1C9F02BD6B2F9003FCE3C /* CustomHTTPCookie.swift */; };
+ 3AF7D3BB2BF3CF410056F497 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF7D3BA2BF3CF410056F497 /* Module.swift */; };
+ A50981B92CEBDF0800F4B487 /* PingLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A50981B72CEBDF0800F4B487 /* PingLogger.framework */; };
+ A50981BA2CEBDF0800F4B487 /* PingLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A50981B72CEBDF0800F4B487 /* PingLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A50981BB2CEBDF0800F4B487 /* PingStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A50981B82CEBDF0800F4B487 /* PingStorage.framework */; };
+ A50981BC2CEBDF0800F4B487 /* PingStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A50981B82CEBDF0800F4B487 /* PingStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A51D4CE32C62EB4B00FE09E0 /* CustomHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CE22C62EB4B00FE09E0 /* CustomHeader.swift */; };
+ A51D4CFC2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CFB2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift */; };
+ A51D4CFE2C6BAAE900FE09E0 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CFD2C6BAAE900FE09E0 /* MockURLProtocol.swift */; };
+ A51D4D002C6BABEF00FE09E0 /* MockAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4CFF2C6BABEF00FE09E0 /* MockAPIEndpoint.swift */; };
+ A51D4D272C6C1B2800FE09E0 /* NodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4D262C6C1B2800FE09E0 /* NodeTests.swift */; };
+ A51D4D292C6C1EA700FE09E0 /* CookieModuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4D282C6C1EA700FE09E0 /* CookieModuleTests.swift */; };
+ A51D4D2B2C6C222600FE09E0 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4D2A2C6C222600FE09E0 /* SessionTests.swift */; };
+ A51D4D2D2C6C260400FE09E0 /* WorkflowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D4D2C2C6C260400FE09E0 /* WorkflowTests.swift */; };
+ A5A712462CAC523B00B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A712452CAC523B00B7DD58 /* PrivacyInfo.xcprivacy */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 3A5441802BCDF1D900385131 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3A54416B2BCDF1D900385131 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 3A5441732BCDF1D900385131;
+ remoteInfo = PingOrchestrate;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 3AB7D7CE2C012C0000CA4A02 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ A50981BC2CEBDF0800F4B487 /* PingStorage.framework in Embed Frameworks */,
+ A50981BA2CEBDF0800F4B487 /* PingLogger.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 3A12F9BD2BCE20B50087DF67 /* Workflow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Workflow.swift; sourceTree = ""; };
+ 3A203D582BD9DC600020C995 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; };
+ 3A203D5A2BD9E2C80020C995 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; };
+ 3A203D762BDA1CEC0020C995 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
+ 3A203DA12BE0312A0020C995 /* HttpClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpClient.swift; sourceTree = ""; };
+ 3A203DA32BE068BE0020C995 /* WorkflowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowConfig.swift; sourceTree = ""; };
+ 3A203DA52BE068E10020C995 /* ModuleRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleRegistry.swift; sourceTree = ""; };
+ 3A203DA72BE069120020C995 /* Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setup.swift; sourceTree = ""; };
+ 3A203DA92BE06DFF0020C995 /* CookieModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieModule.swift; sourceTree = ""; };
+ 3A2E868F2C179E2F000EC6E2 /* FlowContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowContextTests.swift; sourceTree = ""; };
+ 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingOrchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3A5441772BCDF1D900385131 /* Orchestrate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Orchestrate.h; sourceTree = ""; };
+ 3A54417E2BCDF1D900385131 /* OrchestrateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OrchestrateTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3A5441832BCDF1D900385131 /* OrchestrateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrchestrateTests.swift; sourceTree = ""; };
+ 3A7575752C063F2A00891EC7 /* ModuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleTests.swift; sourceTree = ""; };
+ 3A7575772C0673A100891EC7 /* ResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseTests.swift; sourceTree = ""; };
+ 3A7575792C06947000891EC7 /* SharedContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedContext.swift; sourceTree = ""; };
+ 3A75757B2C06B59F00891EC7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; };
+ 3AB1C9E52BD6410A003FCE3C /* Response.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; };
+ 3AB1C9F02BD6B2F9003FCE3C /* CustomHTTPCookie.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomHTTPCookie.swift; sourceTree = ""; };
+ 3AF7D3BA2BF3CF410056F497 /* Module.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Module.swift; sourceTree = ""; };
+ A50981B72CEBDF0800F4B487 /* PingLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A50981B82CEBDF0800F4B487 /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A51D4CE22C62EB4B00FE09E0 /* CustomHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHeader.swift; sourceTree = ""; };
+ A51D4CFB2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHeaderModuleTests.swift; sourceTree = ""; };
+ A51D4CFD2C6BAAE900FE09E0 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; };
+ A51D4CFF2C6BABEF00FE09E0 /* MockAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIEndpoint.swift; sourceTree = ""; };
+ A51D4D262C6C1B2800FE09E0 /* NodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeTests.swift; sourceTree = ""; };
+ A51D4D282C6C1EA700FE09E0 /* CookieModuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieModuleTests.swift; sourceTree = ""; };
+ A51D4D2A2C6C222600FE09E0 /* SessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; };
+ A51D4D2C2C6C260400FE09E0 /* WorkflowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowTests.swift; sourceTree = ""; };
+ A5A712452CAC523B00B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 3A5441712BCDF1D900385131 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A50981BB2CEBDF0800F4B487 /* PingStorage.framework in Frameworks */,
+ A50981B92CEBDF0800F4B487 /* PingLogger.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A54417B2BCDF1D900385131 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3A54417F2BCDF1D900385131 /* PingOrchestrate.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 3A54416A2BCDF1D900385131 = {
+ isa = PBXGroup;
+ children = (
+ 3A203D762BDA1CEC0020C995 /* README.md */,
+ 3A5441762BCDF1D900385131 /* Orchestrate */,
+ 3A5441822BCDF1D900385131 /* OrchestrateTests */,
+ 3A5441752BCDF1D900385131 /* Products */,
+ 3AB7D7CA2C012C0000CA4A02 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 3A5441752BCDF1D900385131 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */,
+ 3A54417E2BCDF1D900385131 /* OrchestrateTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 3A5441762BCDF1D900385131 /* Orchestrate */ = {
+ isa = PBXGroup;
+ children = (
+ A5A712452CAC523B00B7DD58 /* PrivacyInfo.xcprivacy */,
+ 3A5441772BCDF1D900385131 /* Orchestrate.h */,
+ 3A203DA92BE06DFF0020C995 /* CookieModule.swift */,
+ A51D4CE22C62EB4B00FE09E0 /* CustomHeader.swift */,
+ 3A203DA12BE0312A0020C995 /* HttpClient.swift */,
+ 3AF7D3BA2BF3CF410056F497 /* Module.swift */,
+ 3A203DA52BE068E10020C995 /* ModuleRegistry.swift */,
+ 3A203D5A2BD9E2C80020C995 /* Node.swift */,
+ 3AB1C9F02BD6B2F9003FCE3C /* CustomHTTPCookie.swift */,
+ 3A203D582BD9DC600020C995 /* Request.swift */,
+ 3AB1C9E52BD6410A003FCE3C /* Response.swift */,
+ 3A203DA72BE069120020C995 /* Setup.swift */,
+ 3A7575792C06947000891EC7 /* SharedContext.swift */,
+ 3A12F9BD2BCE20B50087DF67 /* Workflow.swift */,
+ 3A203DA32BE068BE0020C995 /* WorkflowConfig.swift */,
+ );
+ path = Orchestrate;
+ sourceTree = "";
+ };
+ 3A5441822BCDF1D900385131 /* OrchestrateTests */ = {
+ isa = PBXGroup;
+ children = (
+ A509D1CB2C6D03AC003A0006 /* mock */,
+ A51D4D282C6C1EA700FE09E0 /* CookieModuleTests.swift */,
+ A51D4CFB2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift */,
+ 3A2E868F2C179E2F000EC6E2 /* FlowContextTests.swift */,
+ 3A7575752C063F2A00891EC7 /* ModuleTests.swift */,
+ A51D4D262C6C1B2800FE09E0 /* NodeTests.swift */,
+ 3A5441832BCDF1D900385131 /* OrchestrateTests.swift */,
+ 3A75757B2C06B59F00891EC7 /* RequestTests.swift */,
+ 3A7575772C0673A100891EC7 /* ResponseTests.swift */,
+ A51D4D2A2C6C222600FE09E0 /* SessionTests.swift */,
+ A51D4D2C2C6C260400FE09E0 /* WorkflowTests.swift */,
+ );
+ path = OrchestrateTests;
+ sourceTree = "";
+ };
+ 3AB7D7CA2C012C0000CA4A02 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ A50981B72CEBDF0800F4B487 /* PingLogger.framework */,
+ A50981B82CEBDF0800F4B487 /* PingStorage.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ A509D1CB2C6D03AC003A0006 /* mock */ = {
+ isa = PBXGroup;
+ children = (
+ A51D4CFD2C6BAAE900FE09E0 /* MockURLProtocol.swift */,
+ A51D4CFF2C6BABEF00FE09E0 /* MockAPIEndpoint.swift */,
+ );
+ path = mock;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+ 3A54416F2BCDF1D900385131 /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3A5441852BCDF1D900385131 /* Orchestrate.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+ 3A5441732BCDF1D900385131 /* PingOrchestrate */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3A5441882BCDF1D900385131 /* Build configuration list for PBXNativeTarget "PingOrchestrate" */;
+ buildPhases = (
+ 3A54416F2BCDF1D900385131 /* Headers */,
+ 3A5441702BCDF1D900385131 /* Sources */,
+ 3A5441712BCDF1D900385131 /* Frameworks */,
+ 3A5441722BCDF1D900385131 /* Resources */,
+ 3AB7D7CE2C012C0000CA4A02 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PingOrchestrate;
+ productName = PingOrchestrate;
+ productReference = 3A5441742BCDF1D900385131 /* PingOrchestrate.framework */;
+ productType = "com.apple.product-type.framework";
+ };
+ 3A54417D2BCDF1D900385131 /* OrchestrateTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3A54418B2BCDF1D900385131 /* Build configuration list for PBXNativeTarget "OrchestrateTests" */;
+ buildPhases = (
+ 3A54417A2BCDF1D900385131 /* Sources */,
+ 3A54417B2BCDF1D900385131 /* Frameworks */,
+ 3A54417C2BCDF1D900385131 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 3A5441812BCDF1D900385131 /* PBXTargetDependency */,
+ );
+ name = OrchestrateTests;
+ productName = PingOrchestrateTests;
+ productReference = 3A54417E2BCDF1D900385131 /* OrchestrateTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 3A54416B2BCDF1D900385131 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1540;
+ TargetAttributes = {
+ 3A5441732BCDF1D900385131 = {
+ CreatedOnToolsVersion = 15.0;
+ };
+ 3A54417D2BCDF1D900385131 = {
+ CreatedOnToolsVersion = 15.0;
+ };
+ };
+ };
+ buildConfigurationList = 3A54416E2BCDF1D900385131 /* Build configuration list for PBXProject "Orchestrate" */;
+ compatibilityVersion = "Xcode 15.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 3A54416A2BCDF1D900385131;
+ productRefGroup = 3A5441752BCDF1D900385131 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 3A5441732BCDF1D900385131 /* PingOrchestrate */,
+ 3A54417D2BCDF1D900385131 /* OrchestrateTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 3A5441722BCDF1D900385131 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A712462CAC523B00B7DD58 /* PrivacyInfo.xcprivacy in Resources */,
+ 3A203D772BDA1CEC0020C995 /* README.md in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A54417C2BCDF1D900385131 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 3A5441702BCDF1D900385131 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3A203DA62BE068E10020C995 /* ModuleRegistry.swift in Sources */,
+ 3A203DAA2BE06DFF0020C995 /* CookieModule.swift in Sources */,
+ 3A203DA42BE068BE0020C995 /* WorkflowConfig.swift in Sources */,
+ 3A203DA22BE0312A0020C995 /* HttpClient.swift in Sources */,
+ 3AF7D3BB2BF3CF410056F497 /* Module.swift in Sources */,
+ 3A203DA82BE069120020C995 /* Setup.swift in Sources */,
+ 3A203D5B2BD9E2C80020C995 /* Node.swift in Sources */,
+ 3A12F9BE2BCE20B50087DF67 /* Workflow.swift in Sources */,
+ 3AB1C9F22BD6B2F9003FCE3C /* CustomHTTPCookie.swift in Sources */,
+ 3A75757A2C06947000891EC7 /* SharedContext.swift in Sources */,
+ 3AB1C9E92BD6410A003FCE3C /* Response.swift in Sources */,
+ 3A203D592BD9DC600020C995 /* Request.swift in Sources */,
+ A51D4CE32C62EB4B00FE09E0 /* CustomHeader.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A54417A2BCDF1D900385131 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A51D4D2B2C6C222600FE09E0 /* SessionTests.swift in Sources */,
+ A51D4D002C6BABEF00FE09E0 /* MockAPIEndpoint.swift in Sources */,
+ A51D4D272C6C1B2800FE09E0 /* NodeTests.swift in Sources */,
+ 3A2E86902C179E2F000EC6E2 /* FlowContextTests.swift in Sources */,
+ 3A5441842BCDF1D900385131 /* OrchestrateTests.swift in Sources */,
+ 3A75757C2C06B59F00891EC7 /* RequestTests.swift in Sources */,
+ A51D4D2D2C6C260400FE09E0 /* WorkflowTests.swift in Sources */,
+ A51D4CFC2C6BA9BD00FE09E0 /* CustomHeaderModuleTests.swift in Sources */,
+ 3A7575782C0673A100891EC7 /* ResponseTests.swift in Sources */,
+ 3A7575762C063F2A00891EC7 /* ModuleTests.swift in Sources */,
+ A51D4D292C6C1EA700FE09E0 /* CookieModuleTests.swift in Sources */,
+ A51D4CFE2C6BAAE900FE09E0 /* MockURLProtocol.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 3A5441812BCDF1D900385131 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 3A5441732BCDF1D900385131 /* PingOrchestrate */;
+ targetProxy = 3A5441802BCDF1D900385131 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 3A5441862BCDF1D900385131 /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ 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_STRICT_CONCURRENCY = complete;
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Debug;
+ };
+ 3A5441872BCDF1D900385131 /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_STRICT_CONCURRENCY = complete;
+ SWIFT_VERSION = 5.0;
+ VALIDATE_PRODUCT = YES;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Release;
+ };
+ 3A5441892BCDF1D900385131 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Orchestrate;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_STRICT_CONCURRENCY = complete;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 3A54418A2BCDF1D900385131 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Orchestrate;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_STRICT_CONCURRENCY = complete;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 3A54418C2BCDF1D900385131 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.OrchestrateTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_STRICT_CONCURRENCY = minimal;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Debug;
+ };
+ 3A54418D2BCDF1D900385131 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.OrchestrateTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_STRICT_CONCURRENCY = minimal;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 3A54416E2BCDF1D900385131 /* Build configuration list for PBXProject "Orchestrate" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A5441862BCDF1D900385131 /* Debug */,
+ 3A5441872BCDF1D900385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 3A5441882BCDF1D900385131 /* Build configuration list for PBXNativeTarget "PingOrchestrate" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A5441892BCDF1D900385131 /* Debug */,
+ 3A54418A2BCDF1D900385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 3A54418B2BCDF1D900385131 /* Build configuration list for PBXNativeTarget "OrchestrateTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A54418C2BCDF1D900385131 /* Debug */,
+ 3A54418D2BCDF1D900385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 3A54416B2BCDF1D900385131 /* Project object */;
+}
diff --git a/Orchestrate/Orchestrate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Orchestrate/Orchestrate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/Orchestrate/Orchestrate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Orchestrate/Orchestrate.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Orchestrate/Orchestrate.xcodeproj/xcshareddata/IDETemplateMacros.plist
new file mode 100644
index 0000000..16cf018
--- /dev/null
+++ b/Orchestrate/Orchestrate.xcodeproj/xcshareddata/IDETemplateMacros.plist
@@ -0,0 +1,17 @@
+
+
+
+
+ FILEHEADER
+
+// ___FILENAME___
+// ___PACKAGENAME___
+//
+// Copyright (c) ___YEAR___ Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+
diff --git a/Orchestrate/Orchestrate.xcodeproj/xcshareddata/xcschemes/Orchestrate.xcscheme b/Orchestrate/Orchestrate.xcodeproj/xcshareddata/xcschemes/Orchestrate.xcscheme
new file mode 100644
index 0000000..c5c6800
--- /dev/null
+++ b/Orchestrate/Orchestrate.xcodeproj/xcshareddata/xcschemes/Orchestrate.xcscheme
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Orchestrate/Orchestrate/CookieModule.swift b/Orchestrate/Orchestrate/CookieModule.swift
new file mode 100644
index 0000000..f085ea0
--- /dev/null
+++ b/Orchestrate/Orchestrate/CookieModule.swift
@@ -0,0 +1,301 @@
+//
+// CookieModule.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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 PingStorage
+
+/// A module that manages cookies.
+public class CookieModule {
+
+ /// Initializes a new instance of `CookieModule`.
+ public init() {}
+
+ /// The module configuration for managing cookies.
+ public static let config: Module = Module.of({ CookieConfig() }) {
+ setup in
+
+ setup.initialize {
+ setup.context.set(key: SharedContext.Keys.cookieStorage, value: setup.config.cookieStorage)
+ }
+
+ setup.start { context, request in
+ let cookies = try? await setup.config.cookieStorage.get()
+ if let url = request.urlRequest.url, let cookies = cookies {
+ CookieModule.inject(url: url,
+ cookies: cookies,
+ inMemoryStorage: setup.config.inMemoryStorage,
+ request: request)
+ }
+ return request
+ }
+
+ setup.next { context, _, request in
+ if let url = request.urlRequest.url {
+ let allCookies = setup.config.inMemoryStorage.cookies(for: url)
+ if let allCookies = allCookies {
+ request.cookies(cookies: allCookies)
+ }
+ if let cookies = try? await setup.config.cookieStorage.get() {
+ CookieModule.inject(url: url, cookies: cookies, inMemoryStorage: setup.config.inMemoryStorage, request: request)
+ }
+ }
+ return request
+ }
+
+ setup.response { context, response in
+ let cookies = response.getCookies()
+ if cookies.count > 0, let url = response.response.url {
+ await CookieModule.parseResponseForCookie(context: context,
+ url: url,
+ cookies: cookies,
+ storage: setup.config.inMemoryStorage,
+ cookieConfig: setup.config)
+ }
+ }
+
+ setup.signOff { request in
+ if let url = request.urlRequest.url {
+ if let cookies = try? await setup.config.cookieStorage.get() {
+ CookieModule.inject(url: url, cookies: cookies, inMemoryStorage: setup.config.inMemoryStorage, request: request)
+ }
+ try? await setup.config.cookieStorage.delete()
+ setup.config.inMemoryStorage.deleteCookies(url: url)
+ }
+ return request
+ }
+ }
+
+ /// Injects cookies into an HTTP request.
+ /// - Parameters:
+ /// - url: The URL of the request.
+ /// - cookies: The cookies to be injected.
+ /// - inMemoryStorage: In-memory cookie storage.
+ /// - request: The HTTP request to modify.
+ static func inject(url: URL,
+ cookies: [CustomHTTPCookie],
+ inMemoryStorage: InMemoryCookieStorage?,
+ request: Request) {
+
+ inMemoryStorage?.deleteCookies(url: url)
+
+ cookies.compactMap { $0.toHTTPCookie() }
+ .forEach { inMemoryStorage?.setCookie($0) }
+
+ if let cookie = inMemoryStorage?.cookies(for: url) {
+ request.cookies(cookies: cookie)
+ }
+ }
+
+ /// Parses cookies from an HTTP response and updates storage.
+ /// - Parameters:
+ /// - context: The workflow context.
+ /// - url: The URL associated with the response.
+ /// - cookies: The cookies received in the response.
+ /// - storage: In-memory cookie storage.
+ /// - cookieConfig: Configuration for cookie persistence.
+ static func parseResponseForCookie(context: FlowContext,
+ url: URL,
+ cookies: [HTTPCookie],
+ storage: InMemoryCookieStorage?,
+ cookieConfig: CookieConfig) async {
+
+ let persistCookies = cookies.filter { cookieConfig.persist.contains($0.name) }
+ let otherCookies = cookies.filter { !cookieConfig.persist.contains($0.name) }
+
+ storage?.deleteCookies(url: url)
+
+ if !persistCookies.isEmpty {
+
+ // Add existing cookies to cookie storage
+ try? await cookieConfig.cookieStorage.get()?.compactMap { $0.toHTTPCookie() }.forEach {
+ storage?.setCookie($0)
+ }
+
+ // Clear existing cookies from keychain
+ try? await cookieConfig.cookieStorage.delete()
+
+ // Add new cookies to temp cookie storage
+ persistCookies.forEach {
+ storage?.setCookie($0)
+ }
+
+ // Persist only the required cookies to keychain
+ let cookieData = storage?.cookies(for: url)?
+ .filter { cookieConfig.persist.contains($0.name) }
+ .compactMap { value in
+ CustomHTTPCookie(from: value)
+ }
+ if let cookieData = cookieData {
+ try? await cookieConfig.cookieStorage.save(item: cookieData)
+ }
+
+ }
+
+ // Persist non-persist cookies to cookie storage
+ otherCookies.forEach { storage?.setCookie($0) }
+ }
+}
+
+
+/// Configuration for managing cookies in the application.
+public class CookieConfig {
+ typealias Cookies = [String]
+
+ /// A list of Cookies name that should be persisted to the storage. For cookies that should not be persisted, do not add the cookie name to this list.
+ public var persist: [String] = []
+ /// In-memory storage for cookies.
+ public private(set) var inMemoryStorage: InMemoryCookieStorage
+ /// Persistent storage for cookies.
+ public internal(set) var cookieStorage: StorageDelegate<[CustomHTTPCookie]>
+
+ /// Initializes a new instance of `CookieConfig`.
+ public init() {
+ cookieStorage = KeychainStorage<[CustomHTTPCookie]>(account: SharedContext.Keys.cookieStorage, encryptor: SecuredKeyEncryptor() ?? NoEncryptor())
+ inMemoryStorage = InMemoryCookieStorage()
+ }
+}
+
+
+extension Workflow {
+ /// Checks if the workflow has cookies available in storage.
+ /// - Returns: A Boolean value indicating whether cookies exist in the storage.
+ public func hasCookies() async -> Bool {
+ let storage = sharedContext.get(key: SharedContext.Keys.cookieStorage) as? StorageDelegate<[CustomHTTPCookie]>
+ let value = try? await storage?.get()
+ return (value != nil) && (value?.count ?? 0 > 0)
+ }
+}
+
+/// A storage class for managing in-memory cookies.
+public final class InMemoryCookieStorage: HTTPCookieStorage {
+ private var cookieStore: [HTTPCookie] = []
+
+ /// Adds or updates a cookie in the storage.
+ /// - Parameter cookie: The cookie to add or update.
+ public override func setCookie(_ cookie: HTTPCookie) {
+ cookieStore.removeAll { $0.name == cookie.name && $0.domain == cookie.domain && $0.path == cookie.path }
+ cookieStore.append(cookie)
+ }
+
+ /// Deletes a specific cookie from the storage.
+ /// - Parameter cookie: The cookie to delete.
+ public override func deleteCookie(_ cookie: HTTPCookie) {
+ cookieStore.removeAll { $0 == cookie }
+ }
+
+ /// Deletes all cookies associated with a specific URL.
+ /// - Parameter url: The URL whose cookies should be deleted.
+ public func deleteCookies(url: URL) {
+ cookies(for: url)?.forEach { value in
+ deleteCookie(value)
+ }
+ }
+
+ /// Retrieves all cookies currently stored.
+ public override var cookies: [HTTPCookie]? {
+ return cookieStore
+ }
+
+ /// Retrieves cookies associated with a specific URL.
+ /// - Parameter url: The URL to fetch cookies for.
+ public override func cookies(for url: URL) -> [HTTPCookie]? {
+ return cookieStore.filter {!$0.isExpired && $0.validateURL(url) }
+ }
+
+ /// Deletes all cookies from the storage./// Adds multiple cookies to the storage.
+ /// - Parameters:
+ /// - cookies: The cookies to add.
+ /// - url: The URL associated with the cookies (optional).
+ /// - mainDocumentURL: The main document URL (optional).
+ public override func setCookies(_ cookies: [HTTPCookie], for url: URL?, mainDocumentURL: URL?) {
+ for cookie in cookies {
+ setCookie(cookie)
+ }
+ }
+}
+
+
+extension SharedContext.Keys {
+ static let cookieStorage = "COOKIE_STORAGE"
+}
+
+
+extension HTTPCookie {
+ var isExpired: Bool {
+ get {
+ if let expDate = self.expiresDate, expDate.timeIntervalSince1970 < Date().timeIntervalSince1970 {
+ return true
+ }
+ return false
+ }
+ }
+
+ func validateIsSecure(_ url: URL) -> Bool {
+ if !self.isSecure {
+ return true
+ }
+ if let urlScheme = url.scheme, urlScheme.lowercased() == "https" {
+ return true
+ }
+ return false
+ }
+
+ func validateURL(_ url: URL) -> Bool {
+ return self.validateDomain(url: url) && self.validatePath(url: url)
+ }
+
+ private func validatePath(url: URL) -> Bool {
+ let path = url.path.count == 0 ? "/" : url.path
+
+ // For exact matching i.e. /path == /path
+ if path == self.path {
+ return true
+ }
+
+ // For partial matching
+ if path.hasPrefix(self.path) {
+ // if Cookie path ends with /
+ // i.e. /abc == / or /abc/def == /abc/
+ if self.path.hasSuffix("/") {
+ return true
+ }
+
+ // making sure to validate exact path matching
+ // i.e. /abcd != /abc, /abc/def == /abc
+ if path.hasPrefix(self.path + "/") {
+ return true
+ }
+ }
+ return false
+ }
+
+ private func validateDomain(url: URL) -> Bool {
+
+ guard let host = url.host else {
+ // Invalid URL host
+ return false
+ }
+
+ // For exact matching i.e. forgerock.com == forgerock.com or am.forgerock.com == am.forgerock.com
+ if host == self.domain {
+ return true
+ }
+ // For sub domain matching i.e. demo.forgerock.com == .forgerock.com
+ if host.hasSuffix(self.domain) {
+ return true
+ }
+ // For ignoring leading dot
+ if (self.domain.count - host.count == 1) && self.domain.hasPrefix(".") {
+ return true
+ }
+ return false
+ }
+}
diff --git a/Orchestrate/Orchestrate/CustomHTTPCookie.swift b/Orchestrate/Orchestrate/CustomHTTPCookie.swift
new file mode 100644
index 0000000..c67ebc5
--- /dev/null
+++ b/Orchestrate/Orchestrate/CustomHTTPCookie.swift
@@ -0,0 +1,92 @@
+//
+// CustomHTTPCookie.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// A struct that represents a custom HTTP cookie.
+public struct CustomHTTPCookie: Codable {
+ var version: Int
+ var name: String?
+ var value: String?
+ var expiresDate: Date?
+ var isSessionOnly: Bool
+ var domain: String?
+ var path: String?
+ var isSecure: Bool
+ var isHTTPOnly: Bool
+ var comment: String?
+ var commentURL: URL?
+ var portList: [Int]?
+ var sameSitePolicy: String?
+
+ enum CodingKeys: String, CodingKey {
+ case version
+ case name
+ case value
+ case expiresDate
+ case isSessionOnly
+ case domain
+ case path
+ case isSecure
+ case isHTTPOnly
+ case comment
+ case commentURL
+ case portList
+ case sameSitePolicy
+ }
+
+ /// Initializes a `CustomHTTPCookie` from an `HTTPCookie`.
+ /// - Parameter cookie: The `HTTPCookie` to initialize from.
+ public init(from cookie: HTTPCookie) {
+ self.version = cookie.version
+ self.name = cookie.name
+ self.value = cookie.value
+ self.expiresDate = cookie.expiresDate
+ self.isSessionOnly = cookie.isSessionOnly
+ self.domain = cookie.domain
+ self.path = cookie.path
+ self.isSecure = cookie.isSecure
+ self.isHTTPOnly = cookie.isHTTPOnly
+ self.comment = cookie.comment
+ self.commentURL = cookie.commentURL
+ self.portList = cookie.portList?.map { $0.intValue }
+ self.sameSitePolicy = cookie.sameSitePolicy?.rawValue
+ }
+
+ /// Converts the `CustomHTTPCookie` to an `HTTPCookie`.
+ /// - Returns: An `HTTPCookie` instance.
+ public func toHTTPCookie() -> HTTPCookie? {
+ var properties = [HTTPCookiePropertyKey: Any]()
+ properties[.version] = self.version
+ properties[.name] = self.name
+ properties[.value] = self.value
+ properties[.expires] = self.expiresDate
+ properties[.discard] = self.isSessionOnly ? Constants.true : nil
+ properties[.domain] = self.domain
+ properties[.path] = self.path
+ properties[.secure] = self.isSecure ? Constants.true : nil
+ properties[HTTPCookiePropertyKey(Constants.httpOnly)] = self.isHTTPOnly ? Constants.true : nil
+ properties[.comment] = self.comment
+ properties[.commentURL] = self.commentURL
+ properties[.port] = self.portList?.map { NSNumber(value: $0) }
+
+ if let sameSitePolicyValue = self.sameSitePolicy {
+ properties[HTTPCookiePropertyKey.sameSitePolicy] = HTTPCookieStringPolicy(rawValue: sameSitePolicyValue)
+ }
+
+ return HTTPCookie(properties: properties)
+ }
+
+ enum Constants {
+ static let `true` = "TRUE"
+ static let httpOnly = "HttpOnly"
+ }
+}
diff --git a/Orchestrate/Orchestrate/CustomHeader.swift b/Orchestrate/Orchestrate/CustomHeader.swift
new file mode 100644
index 0000000..1db32a6
--- /dev/null
+++ b/Orchestrate/Orchestrate/CustomHeader.swift
@@ -0,0 +1,51 @@
+//
+// CustomHeader.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Configuration class for CustomHeader.
+/// Allows adding custom headers to be injected into requests.
+public class CustomHeaderConfig {
+ internal var headers = [(String, String)]()
+
+ /// Adds a custom header to the configuration.
+ /// - Parameters:
+ /// - name: The name of the header.
+ /// - value: The value of the header.
+ public func header(name: String, value: String) {
+ headers.append((name, value))
+ }
+}
+
+
+/// Module for injecting custom headers into requests.
+public class CustomHeader {
+
+ /// Initializes a new instance of `CustomHeader`.
+ public init() {}
+
+ /// The module configuration.
+ public static let config: Module = Module.of({ CustomHeaderConfig() }) { setup in
+ setup.start { flowContext, request in
+ setup.config.headers.forEach { name, value in
+ request.header(name: name, value: value)
+ }
+ return request
+ }
+
+ setup.next { flowContext, _, request in
+ setup.config.headers.forEach { name, value in
+ request.header(name: name, value: value)
+ }
+ return request
+ }
+ }
+}
diff --git a/Orchestrate/Orchestrate/HttpClient.swift b/Orchestrate/Orchestrate/HttpClient.swift
new file mode 100644
index 0000000..c012151
--- /dev/null
+++ b/Orchestrate/Orchestrate/HttpClient.swift
@@ -0,0 +1,102 @@
+//
+// HttpClient.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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 PingLogger
+
+/// `HttpClient` is responsible for handling HTTP requests and logging the details of those requests and responses.
+public class HttpClient {
+ var session: URLSession
+ /// The timeout interval for HTTP requests.
+ public var timeoutIntervalForRequest: TimeInterval = 60.0
+
+ /// Initializes a new instance of `HttpClient`.
+ /// - Parameter session: The URLSession instance to be used for HTTP requests. Defaults to a session with `RedirectPreventer` delegate.
+ public init(session: URLSession = URLSession(configuration: URLSessionConfiguration.default,
+ delegate: RedirectPreventer(), delegateQueue: nil)) {
+ self.session = session
+ }
+
+ /// Logs the details of an HTTP request.
+ /// - Parameter request: The URLRequest to be logged.
+ public func logRequest(request: URLRequest?) {
+ if let request = request {
+ var log = "⬆\n"
+ log += "Request URL: \(request.url?.absoluteString ?? "")\n"
+ log += "Request Method: \(request.httpMethod ?? "")\n"
+ if let headers = request.allHTTPHeaderFields {
+ log += "Request Headers: \(headers)\n"
+ }
+ if let bodyData = request.httpBody, let bodyString = String(data: bodyData, encoding: .utf8) {
+ log += "Request Body: \(bodyString)\n"
+ }
+ log += "Request Timeout: \(request.timeoutInterval)\n"
+ LogManager.standard.d(log)
+ }
+ }
+
+ /// Logs the details of an HTTP response.
+ /// - Parameter responseData: The data returned by the server.
+ /// - Parameter response: The URLResponse object containing the response metadata.
+ func logResponse(responseData: Data?, response: URLResponse?) {
+ var log = "⬇\n"
+ if let httpResponse = response as? HTTPURLResponse {
+ log += "Response Status Code: \(httpResponse.statusCode)\n"
+ log += "Response Headers: \(httpResponse.allHeaderFields)\n"
+ log += "Response Value: \(httpResponse.debugDescription)\n"
+ }
+
+ if let data = responseData, let dataString = String(data: data, encoding: .utf8) {
+ log += "Response Data: \(dataString)"
+ }
+ LogManager.standard.d(log)
+ }
+
+ /// Sends an HTTP request and returns the response data and metadata.
+ /// - Parameter request: The URLRequest to be sent.
+ /// - Throws: An error if the request fails.
+ /// - Returns: A tuple containing the response data and metadata.
+ func sendRequest(request: URLRequest) async throws -> (Data, URLResponse) {
+ var request = request
+ request.timeoutInterval = timeoutIntervalForRequest
+ logRequest(request: request)
+ let (data, response) = try await session.data(for: request)
+ logResponse(responseData: data, response: response)
+ return (data, response)
+ }
+
+ /// Sends an HTTP request and returns the response data and metadata.
+ /// - Parameter request: The Request object to be sent.
+ /// - Throws: An error if the request fails.
+ /// - Returns: A tuple containing the response data and metadata.
+ public func sendRequest(request: Request) async throws -> (Data, URLResponse) {
+ return try await sendRequest(request: request.urlRequest)
+ }
+}
+
+
+/// RedirectPreventer` is a delegate class that prevents HTTP redirects during URL sessions.
+/// This class conforms to `URLSessionDelegate` and `URLSessionTaskDelegate` to handle the redirection logic.
+/// It ensures that any HTTP redirection responses are not followed by the `URLSession`.
+public final class RedirectPreventer: NSObject, URLSessionDelegate, URLSessionTaskDelegate {
+ /// Called when the session receives a redirection response.
+ /// This method prevents the redirection by passing `nil` to the `completionHandler`.
+ /// - Parameters:
+ /// - session: The session containing the task that received a redirect.
+ /// - task: The task whose request resulted in a redirect.
+ /// - response: The response that caused the redirect.
+ /// - request: A URL request object filled out with the new location.
+ /// - completionHandler: A closure to call with the URL request to allow the redirection, or `nil` to prevent it.
+ public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
+ // Prevent the redirect by passing nil to the completionHandler
+ completionHandler(nil)
+ }
+}
diff --git a/Orchestrate/Orchestrate/Module.swift b/Orchestrate/Orchestrate/Module.swift
new file mode 100644
index 0000000..4d02173
--- /dev/null
+++ b/Orchestrate/Orchestrate/Module.swift
@@ -0,0 +1,51 @@
+//
+// Module.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// A Module represents a unit of functionality in the application.
+/// - property config: A function that returns the configuration for the module.
+/// - property setup: A function that sets up the module.
+public class Module: Equatable {
+ public private(set) var setup: (Setup) -> (Void)
+ public private(set) var config: () -> (ModuleConfig)
+ /// The unique identifier of the module.
+ public var id: UUID = UUID()
+
+ /// Constructs a module with config.
+ /// - Parameters:
+ /// - config: A function that returns the configuration for the module.
+ /// - setup: A function that sets up the module.
+ public init(config: @escaping () -> (ModuleConfig), setup: @escaping (Setup) -> (Void)) {
+ self.setup = setup
+ self.config = config
+ }
+
+ /// Constructs a module with config.
+ /// - Parameters:
+ /// - config: A function that returns the configuration for the module.
+ /// - setup: A function that sets up the module.
+ /// - Returns: A Module with the provided config.
+ public static func of(_ config: @escaping (() -> (ModuleConfig)) = {},
+ setup: @escaping (Setup) -> (Void)
+ ) -> Module {
+ return Module(config: config, setup: setup)
+ }
+
+ /// Compares two modules.
+ /// - Parameters:
+ /// - lhs: The left-hand module.
+ /// - rhs: The right-hand module.
+ /// - Returns: A boolean value indicating whether the two modules are equal.
+ public static func == (lhs: Module, rhs: Module) -> Bool {
+ return lhs.id == rhs.id
+ }
+}
diff --git a/Orchestrate/Orchestrate/ModuleRegistry.swift b/Orchestrate/Orchestrate/ModuleRegistry.swift
new file mode 100644
index 0000000..41a879e
--- /dev/null
+++ b/Orchestrate/Orchestrate/ModuleRegistry.swift
@@ -0,0 +1,79 @@
+//
+// ModuleRegistry.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Represents a ModuleRegistry protocol. A ModuleRegistry represents a registry of modules in the application.
+public protocol ModuleRegistryProtocol {
+ associatedtype Config: Any
+ /// The UUID of the module
+ var id: UUID { get set }
+ /// The priority of the module in the registry.
+ var priority: Int { get }
+ /// The configuration for the module.
+ var config: Config { get }
+ /// The function that sets up the module.
+ var setup: (Setup) -> (Void) { get }
+
+ /// Registers the module to the workflow.
+ func register(workflow: Workflow)
+}
+
+
+/// Class for a ModuleRegistry. A ModuleRegistry represents a registry of modules in the application.
+/// - property id: The UUID of the module
+/// - property priority: The priority of the module in the registry.
+/// - property config: The configuration for the module.
+/// - property setup: The function that sets up the module.
+public class ModuleRegistry: ModuleRegistryProtocol {
+ public var id: UUID = UUID()
+ public let priority: Int
+ public let config: Config
+ public let setup: (Setup) -> Void
+
+ public init(setup: @escaping (Setup) -> (Void),
+ priority: Int,
+ id: UUID,
+ config: Config) {
+ self.id = id
+ self.priority = priority
+ self.config = config
+ self.setup = setup
+ }
+
+ /// Registers the module to the workflow.
+ /// - parameter workflow: The workflow to which the module is registered.
+ public func register(workflow: Workflow) {
+ let setupInstance = Setup(workflow: workflow, config: config)
+ setup(setupInstance)
+ }
+}
+
+
+extension ModuleRegistry: Comparable {
+ /// Compares two ModuleRegistry instances.
+ /// - Parameters:
+ /// - lhs: The left-hand side ModuleRegistry instance.
+ /// - rhs: The right-hand side ModuleRegistry instance.
+ /// - Returns: A boolean value indicating whether the left-hand side ModuleRegistry instance is less than the right-hand side ModuleRegistry instance.
+ public static func < (lhs: ModuleRegistry, rhs: ModuleRegistry) -> Bool {
+ return lhs.priority < rhs.priority
+ }
+
+ /// Compares two ModuleRegistry instances for equality.
+ /// - Parameters:
+ /// - lhs: The left-hand side ModuleRegistry instance
+ /// - rhs: The right-hand side ModuleRegistry instance
+ /// - Returns: A boolean value indicating whether the left-hand side ModuleRegistry instance is equal to the right-hand side ModuleRegistry instance.
+ public static func == (lhs: ModuleRegistry, rhs: ModuleRegistry) -> Bool {
+ return lhs.priority == rhs.priority
+ }
+}
diff --git a/Orchestrate/Orchestrate/Node.swift b/Orchestrate/Orchestrate/Node.swift
new file mode 100644
index 0000000..0211f15
--- /dev/null
+++ b/Orchestrate/Orchestrate/Node.swift
@@ -0,0 +1,146 @@
+//
+// Node.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+/// Protocol for actions
+public protocol Action {}
+
+
+/// Protocol for closeable resources
+public protocol Closeable {
+ func close()
+}
+
+
+/// Protocol for Node. Represents a node in the workflow.
+public protocol Node {}
+
+
+/// Represents an EmptyNode node in the workflow.
+public struct EmptyNode: Node {
+ /// Initializes a new instance of `EmptyNode`.
+ public init() {}
+}
+
+
+/// Represents an Failure node in the workflow.
+/// - property cause: The cause of the error.
+public struct FailureNode: Node {
+ /// The cause of the error.
+ public let cause: Error
+
+ /// Initializes a new instance of `FailureNode`.
+ /// - Parameter cause: The cause of the error.
+ public init(cause: any Error) {
+ self.cause = cause
+ }
+}
+
+
+/// Represents a ErrorNode node in the workflow.
+/// - property status: The status of the error.
+/// - property input: The input for the error.
+/// - property message: The message for the error.
+public struct ErrorNode: Node {
+ public let input: [String: Any]
+ public let message: String
+ public let status: Int?
+
+ /// Initializes a new instance of `ErrorNode`.
+ /// - Parameters:
+ /// - status: The status of the error.
+ /// - input: The input for the error.
+ /// - message: The message for the error.
+ public init(status: Int? = nil,
+ input: [String : Any] = [:],
+ message: String = "") {
+ self.input = input
+ self.message = message
+ self.status = status
+ }
+}
+
+
+/// Represents a success node in the workflow.
+/// - property input: The input for the success.
+/// - property session: The session for the success.
+public struct SuccessNode: Node {
+ public let input: [String: Any]
+ public let session: Session
+
+ /// Initializes a new instance of `SuccessNode`.
+ /// - Parameters:
+ /// - input: The input for the success.
+ /// - session: The session for the success.
+ public init(input: [String : Any] = [:], session: Session) {
+ self.session = session
+ self.input = input
+ }
+}
+
+
+/// Abstract class for a ContinueNode node in the workflow.
+/// - property context: The context for the node.
+/// - property workflow: The workflow for the node.
+/// - property input: The input for the node.
+/// - property actions: The actions for the node.
+open class ContinueNode: Node, Closeable {
+ public let context: FlowContext
+ public let workflow: Workflow
+ public let input: [String: Any]
+ public let actions: [any Action]
+
+ /// Initializes a new instance of `ContinueNode`.
+ /// - Parameters:
+ /// - context: The context for the node.
+ /// - workflow: The workflow for the node.
+ /// - input: The input for the node.
+ /// - actions: The actions for the node.
+ public init(context: FlowContext, workflow: Workflow, input: [String: Any], actions: [any Action]) {
+ self.context = context
+ self.workflow = workflow
+ self.input = input
+ self.actions = actions
+ }
+
+ /// Converts the ContinueNode to a Request.
+ /// - Returns: The Request representation of the ContinueNode.
+ open func asRequest() -> Request {
+ fatalError("Must be overridden in subclass")
+ }
+
+ /// Moves to the next node in the workflow.
+ /// - Returns: The next Node.
+ public func next() async -> Node {
+ return await workflow.next(context, self)
+ }
+
+ /// Closes all closeable actions.
+ public func close() {
+ actions.compactMap { $0 as? Closeable }.forEach { $0.close() }
+ }
+}
+
+
+/// Protocol for a Session. A Session represents a user's session in the application.
+public protocol Session {
+ /// Returns the value of the session as a String.
+ var value: String { get }
+}
+
+
+/// Singleton for an EmptySession. An EmptySession represents a session with no value.
+public struct EmptySession: Session {
+ /// The value of the empty session as a String.
+ public var value: String = ""
+
+ /// Initializes a new instance of `EmptySession`.
+ public init() {}
+}
diff --git a/Orchestrate/Orchestrate/Orchestrate.h b/Orchestrate/Orchestrate/Orchestrate.h
new file mode 100644
index 0000000..193c1e7
--- /dev/null
+++ b/Orchestrate/Orchestrate/Orchestrate.h
@@ -0,0 +1,21 @@
+//
+// Orchestrate.h
+// Orchestrate
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+#import
+
+//! Project version number for Orchestrate.
+FOUNDATION_EXPORT double OrchestrateVersionNumber;
+
+//! Project version string for Orchestrate.
+FOUNDATION_EXPORT const unsigned char OrchestrateVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+
+
diff --git a/Orchestrate/Orchestrate/PrivacyInfo.xcprivacy b/Orchestrate/Orchestrate/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..fcfc9b9
--- /dev/null
+++ b/Orchestrate/Orchestrate/PrivacyInfo.xcprivacy
@@ -0,0 +1,10 @@
+
+
+
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTracking
+
+
+
diff --git a/Orchestrate/Orchestrate/Request.swift b/Orchestrate/Orchestrate/Request.swift
new file mode 100644
index 0000000..69d062d
--- /dev/null
+++ b/Orchestrate/Orchestrate/Request.swift
@@ -0,0 +1,122 @@
+//
+// SampleRequest.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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 UIKit
+
+/// Class for a Request. A Request represents a request to be sent over the network.
+public class Request {
+
+ /// The URL request.
+ public private(set) var urlRequest: URLRequest = URLRequest(url: URL(string: "https://")!)
+
+ /// Initializes a Request with a URL.
+ /// - Parameter urlString: The URL of the request.
+ public init(urlString: String = "https://") {
+ self.urlRequest.url = URL(string: urlString)!
+ }
+
+ /// Sets the URL of the request.
+ /// - Parameter urlString: The URL to be set.
+ public func url(_ urlString: String) {
+ if let url = URL(string: urlString) {
+ self.urlRequest.url = url
+ // keeping Default Content type
+ self.header(name: Constants.contentType, value: ContentType.json.rawValue)
+ }
+ }
+
+ /// Adds a parameter to the request.
+ /// - Parameters:
+ /// - name: The name of the parameter.
+ /// - value: The value of the parameter.
+ public func parameter(name: String, value: String) {
+ if let url = self.urlRequest.url {
+ if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
+ if components.queryItems == nil {
+ components.queryItems = []
+ }
+ components.queryItems?.append(URLQueryItem(name: name, value: value))
+ if let updatedURL = components.url {
+ self.urlRequest.url = updatedURL
+ }
+ }
+ }
+ }
+
+ /// Adds a header to the request.
+ /// - Parameters:
+ /// - name: The name of the header.
+ /// - value: The value of the header.
+ public func header(name: String, value: String) {
+ self.urlRequest.setValue(value, forHTTPHeaderField: name)
+ }
+
+ /// Adds cookies to the request.
+ /// - Parameter cookies: The cookies to be added.
+ public func cookies(cookies: [HTTPCookie]) {
+ let headers = HTTPCookie.requestHeaderFields(with: cookies)
+ for (key, value) in headers {
+ self.urlRequest.addValue(value, forHTTPHeaderField: key)
+ }
+ }
+
+ /// Sets the body of the request.
+ /// - Parameter body: The body to be set.
+ public func body(body: [String: Any]) {
+ self.urlRequest.httpMethod = HTTPMethod.post.rawValue
+ self.urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: Constants.contentType)
+ self.urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
+ }
+
+ /// Sets the form of the request.
+ /// - Parameter formData: The form to be set.
+ public func form(formData: [String: String]) {
+ var formString = ""
+ for (key, value) in formData {
+ formString += "\(key)=\(value)&"
+ }
+ if !formString.isEmpty {
+ formString.removeLast() // Remove the last '&' character
+ }
+
+ self.urlRequest.httpMethod = HTTPMethod.post.rawValue
+ self.urlRequest.setValue(ContentType.urlEncoded.rawValue, forHTTPHeaderField: Constants.contentType)
+ self.urlRequest.httpBody = formString.data(using: .utf8)
+ }
+
+ /// Represents various content types used in HTTP requests.
+ public enum ContentType: String {
+ case plainText = "text/plain"
+ case json = "application/json"
+ case urlEncoded = "application/x-www-form-urlencoded"
+ }
+
+ /// Represents HTTP methods used in network requests.
+ public enum HTTPMethod: String {
+ case get = "GET"
+ case put = "PUT"
+ case post = "POST"
+ case delete = "DELETE"
+ }
+
+ /// Represents various constants used in network requests.
+ public enum Constants {
+ public static let contentType = "Content-Type"
+ public static let accept = "Accept"
+ public static let xRequestedWith = "x-requested-with"
+ public static let xRequestedPlatform = "x-requested-platform"
+ public static let pingSdk = "ping-sdk"
+ public static let ios = "ios"
+ public static let stCookie = "ST"
+ public static let stNoSsCookie = "ST-NO-SS"
+ }
+}
diff --git a/Orchestrate/Orchestrate/Response.swift b/Orchestrate/Orchestrate/Response.swift
new file mode 100644
index 0000000..31ab14a
--- /dev/null
+++ b/Orchestrate/Orchestrate/Response.swift
@@ -0,0 +1,58 @@
+//
+// SampleRequest.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Struct for a Response. A Response represents a response received from a network request.
+/// - property data: The data received from the network request.
+/// - property response: The URLResponse received from the network request.
+public struct Response {
+ public let data: Data
+ public let response: URLResponse
+
+ /// Returns the body of the response.
+ /// - Returns: The body of the response as a String.
+ public func body() -> String {
+ return String(data: data, encoding: .utf8) ?? ""
+ }
+
+ /// Returns the body of the response as a JSON object.
+ /// - Parameter data: The data to convert to a JSON object.
+ /// - Returns: The body of the response as a JSON object.
+ public func json(data: Data) throws -> [String: Any] {
+ return (try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) ?? [:]
+ }
+
+ /// Returns the status code of the response.
+ /// - Returns: The status code of the response as an Int.
+ public func status() -> Int {
+ return (response as? HTTPURLResponse)?.statusCode ?? 0
+ }
+
+ /// Returns the value of a specific header from the response.
+ /// - Parameter name: The name of the header.
+ /// - Returns: The value of the header as a String.
+ public func header(name: String) -> String? {
+ return (response as? HTTPURLResponse)?.allHeaderFields[name] as? String
+ }
+
+ /// Returns the cookies from the response.
+ /// - Returns: The cookies from the response as an array of HTTPCookie.
+ public func getCookies() -> [HTTPCookie] {
+ if let response = (response as? HTTPURLResponse),
+ let allHeaders = response.allHeaderFields as? [String : String],
+ let url = response.url {
+ let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaders, for: url)
+ return cookies
+ }
+ return []
+ }
+}
diff --git a/Orchestrate/Orchestrate/Setup.swift b/Orchestrate/Orchestrate/Setup.swift
new file mode 100644
index 0000000..e0d7f8d
--- /dev/null
+++ b/Orchestrate/Orchestrate/Setup.swift
@@ -0,0 +1,87 @@
+//
+// Setup.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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 PingLogger
+
+/// Struct for a Setup. A Setup represents the setup of a module in the application.
+/// - property workflow: The workflow of the application.
+/// - property context: The shared context of the application.
+/// - property logger: The logger used in the application.
+/// - property httpClient: The HTTP client used in the application.
+/// - property config: The configuration for the module.
+public struct Setup {
+ public let workflow: Workflow
+ public let context: SharedContext
+ public let logger: Logger
+ public let httpClient: HttpClient
+ public let config: ModuleConfig
+
+ /// Initializes a new Setup instance.
+ /// - Parameters:
+ /// - workflow: The workflow of the application.
+ /// - config: The configuration for the module.
+ public init(workflow: Workflow, config: ModuleConfig) {
+ self.workflow = workflow
+ self.context = workflow.sharedContext
+ self.logger = workflow.config.logger
+ self.httpClient = workflow.config.httpClient
+ self.config = config
+ }
+
+ /// Adds an initialization block to the workflow.
+ /// - Parameter block: The block to be added.
+ public func initialize(block: @escaping() async throws -> Void) {
+ workflow.initHandlers.append(block)
+ }
+
+ /// Adds a start block to the workflow.
+ /// - Parameter block: The block to be added.
+ public func start(_ block: @escaping (FlowContext, Request) async throws -> Request) {
+ workflow.startHandlers.append(block)
+ }
+
+ /// Adds a next block to the workflow.
+ /// - Parameter block: The block to be added.
+ public func next(block: @escaping (FlowContext, ContinueNode, Request) async throws -> Request) {
+ workflow.nextHandlers.append(block)
+ }
+
+ /// Adds a response block to the workflow.
+ /// - Parameter block: The block to be added.
+ public func response(block: @escaping (FlowContext, Response) async throws -> Void) {
+ workflow.responseHandlers.append(block)
+ }
+
+ /// Adds a node block to the workflow.
+ /// - Parameter block: The block to be added.
+ public func node(block: @escaping (FlowContext, Node) async throws -> Node) {
+ workflow.nodeHandlers.append(block)
+ }
+
+ /// Adds a success block to the workflow.
+ /// - Parameter block: The block to be added.
+ public func success(block: @escaping (FlowContext, SuccessNode) async throws -> SuccessNode) {
+ workflow.successHandlers.append(block)
+ }
+
+ /// Sets the transform block of the workflow.
+ /// - Parameter block: The block to be set.
+ public func transform(block: @escaping (FlowContext, Response) async throws -> Node) {
+ workflow.transformHandler = block
+ }
+
+ /// Adds a sign off block to the workflow.
+ /// - Parameter block: The block to be added.
+ public func signOff(block: @escaping (Request) async -> Request) {
+ workflow.signOffHandlers.append(block)
+ }
+}
diff --git a/Orchestrate/Orchestrate/SharedContext.swift b/Orchestrate/Orchestrate/SharedContext.swift
new file mode 100644
index 0000000..de426ae
--- /dev/null
+++ b/Orchestrate/Orchestrate/SharedContext.swift
@@ -0,0 +1,68 @@
+//
+// SharedContext.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// A class that manages a shared context using a dictionary.
+public class SharedContext {
+ private var map: [String: Any] = [:]
+ private var queue = DispatchQueue(label: "shared.conext.queue", attributes: .concurrent)
+
+ /// Initializes the SharedContext with an empty dictionary or a pre-existing one.
+ ///
+ /// - Parameter map: A dictionary to initialize the context with. Defaults to an empty dictionary.
+ public init(_ map: [String: Any] = [:]) {
+ queue.sync(flags: .barrier) {
+ self.map = map
+ }
+ }
+
+ /// Sets a value for the given key in the shared context.
+ ///
+ /// - Parameters:
+ /// - key: The key for which to set the value.
+ /// - value: The value to set for the given key.
+ public func set(key: String, value: Any) {
+ queue.sync(flags: .barrier) {
+ self.map[key] = value
+ }
+ }
+
+ /// Retrieves the value for the given key from the shared context.
+ ///
+ /// - Parameter key: The key for which to get the value.
+ /// - Returns: The value associated with the key, or `nil` if the key does not exist.
+ public func get(key: String) -> Any? {
+ queue.sync {
+ return self.map[key]
+ }
+ }
+
+ /// Removes the value for the given key from the shared context.
+ ///
+ /// - Parameter key: The key for which to remove the value.
+ /// - Returns: The removed value, or `nil` if the key does not exist.
+ public func removeValue(forKey key: String) -> Any? {
+ queue.sync(flags: .barrier) {
+ self.map.removeValue(forKey: key)
+ }
+ }
+
+ /// A Boolean value indicating whether the shared context is empty.
+ public var isEmpty: Bool {
+ queue.sync {
+ return self.map.isEmpty
+ }
+ }
+
+ /// A namespace for key names to be added in an extension.
+ public enum Keys { }
+}
diff --git a/Orchestrate/Orchestrate/WorkFlow.swift b/Orchestrate/Orchestrate/WorkFlow.swift
new file mode 100644
index 0000000..0ba7e60
--- /dev/null
+++ b/Orchestrate/Orchestrate/WorkFlow.swift
@@ -0,0 +1,221 @@
+//
+// Workflow.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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 PingLogger
+
+/// Class representing the context of a flow.
+/// - property flowContext: The shared context of the flow.
+public class FlowContext {
+ public let flowContext: SharedContext
+
+ public init(flowContext: SharedContext) {
+ self.flowContext = flowContext
+ }
+}
+
+
+extension Workflow {
+ /// Creates a new Workflow instance with the provided configuration block.
+ /// - Parameter block: The configuration block for the Workflow.
+ /// - Returns: A new Workflow instance.
+ public static func createWorkflow(_ block: (WorkflowConfig) -> Void = { _ in }) -> Workflow {
+ let config = WorkflowConfig()
+ block(config)
+ return Workflow(config: config)
+ }
+}
+
+
+/// Enum representing the keys for the modules.
+public enum ModuleKeys: String {
+ case customHeader = "customHeader"
+ case nosession = "nosession"
+ case forceAuth = "forceAuth"
+}
+
+
+/// Class representing a workflow.
+public class Workflow {
+ /// The configuration for the workflow.
+ public let config: WorkflowConfig
+ /// Global SharedContext
+ public let sharedContext = SharedContext()
+
+ private var started = false
+
+ internal var initHandlers = [() async throws -> Void]()
+ internal var startHandlers = [(FlowContext, Request) async throws -> Request]()
+ internal var nextHandlers = [(FlowContext, ContinueNode, Request) async throws -> Request]()
+ internal var responseHandlers = [(FlowContext, Response) async throws -> Void]()
+ internal var nodeHandlers = [(FlowContext, Node) async throws -> Node]()
+ internal var successHandlers = [(FlowContext, SuccessNode) async throws -> SuccessNode]()
+ internal var signOffHandlers = [(Request) async throws -> Request]()
+ // Transform response to Node, we can only have one transform
+ internal var transformHandler: (FlowContext, Response) async throws -> Node = { _, _ in EmptyNode() }
+
+ /// Initializes the workflow.
+ /// - Parameter config: The configuration for the workflow.
+ public init(config: WorkflowConfig) {
+ self.config = config
+ self.config.register(workflow: self)
+ }
+
+ /// Initializes the workflow.
+ public func initialize() async throws {
+ if !started {
+ var tasks: [Task] = []
+ // Create tasks for each handler
+ for handler in initHandlers {
+ let task = Task {
+ try await handler()
+ }
+ tasks.append(task)
+ }
+
+ // Await all tasks to complete
+ for task in tasks {
+ try await task.value
+ }
+ started = true
+ }
+ }
+
+ /// Starts the workflow with the provided request.
+ /// - Parameter request: The request to start the workflow with.
+ /// - Returns: The resulting `Node` after processing the workflow.
+ private func start(request: Request) async throws -> Node {
+ // Before we start, make sure all the module init has been completed
+ try await initialize()
+ config.logger.i("Starting...")
+ let context = FlowContext(flowContext: SharedContext())
+ var currentRequest = request
+ for handler in startHandlers {
+ currentRequest = try await handler(context, currentRequest)
+ }
+ let response = try await send(context, request: currentRequest)
+
+ let transform = try await transformHandler(context, response)
+
+ var initialNode = transform
+ for handler in nodeHandlers {
+ initialNode = try await handler(context, initialNode)
+ }
+
+ return try await next(context, initialNode)
+ }
+
+ /// Starts the workflow with a default request.
+ /// - Returns: The resulting `Node` after processing the workflow.
+ public func start() async -> Node {
+ do {
+ return try await start(request: Request())
+ }
+ catch {
+ return FailureNode(cause: error)
+ }
+ }
+
+ /// Sends a request and returns the response.
+ /// - Parameters:
+ /// - context: The context of the flow.
+ /// - request: The request to be sent.
+ /// - Returns: The response received.
+ private func send(_ context: FlowContext, request: Request) async throws -> Response {
+ let (data, urlResponse) = try await config.httpClient.sendRequest(request: request)
+ let response = Response(data: data, response: urlResponse)
+ for handler in responseHandlers {
+ try await handler(context, response)
+ }
+ return response
+ }
+
+ /// Sends a request and returns the response.
+ /// - Parameter request: The request to be sent.
+ /// - Returns: The response received.
+ private func send(_ request: Request) async throws -> Response {
+ // semaphore
+ let (data, urlResponse) = try await config.httpClient.sendRequest(request: request)
+ return Response(data: data, response: urlResponse)
+ }
+
+ /// Processes the next node if it is a success node.
+ /// - Parameters:
+ /// - context: The context of the flow.
+ /// - node: The current node.
+ /// - Returns: The resulting `Node` after processing the next step.
+ private func next(_ context: FlowContext, _ node: Node) async throws -> Node {
+ if let success = node as? SuccessNode {
+ var result = success
+ for handler in successHandlers {
+ result = try await handler(context, result)
+ }
+ return result
+ } else {
+ return node
+ }
+ }
+
+ /// Processes the next node in the workflow.
+ /// - Parameters:
+ /// - context: The context of the flow.
+ /// - current: The current ContinueNode.
+ /// - Returns: The resulting Node after processing the next step.
+ public func next(_ context: FlowContext, _ current: ContinueNode) async -> Node {
+ do {
+ config.logger.i("Next...")
+ let initialRequest = current.asRequest()
+ var request = initialRequest
+ for handler in nextHandlers {
+ request = try await handler(context, current, request)
+ }
+ current.close()
+ let initialNode = try await transformHandler(context, try await send(context, request: request))
+ var node = initialNode
+ for handler in nodeHandlers {
+ node = try await handler(context, node)
+ }
+ return try await next(context, node)
+ }
+ catch {
+ return FailureNode(cause: error)
+ }
+ }
+
+ /// Signs off the workflow.
+ /// - Returns: A Result indicating the success or failure of the sign off.
+ public func signOff() async -> Result {
+ self.config.logger.i("SignOff...")
+ do {
+ try await initialize()
+ var request = Request()
+ for handler in signOffHandlers {
+ request = try await handler(request)
+ }
+ _ = try await send(request)
+ return .success(())
+ }
+ catch {
+ config.logger.e("Error during sign off", error: error)
+ return .failure(error)
+ }
+ }
+
+ /// Processes the response.
+ /// - Parameters:
+ /// - context: The context of the flow.
+ /// - response: The response to be processed.
+ private func response(context: FlowContext, response: Response) async throws {
+ for handler in responseHandlers {
+ try await handler(context, response)
+ }
+ }
+}
diff --git a/Orchestrate/Orchestrate/WorkFlowConfig.swift b/Orchestrate/Orchestrate/WorkFlowConfig.swift
new file mode 100644
index 0000000..99fb4e2
--- /dev/null
+++ b/Orchestrate/Orchestrate/WorkFlowConfig.swift
@@ -0,0 +1,91 @@
+//
+// WorkflowConfig.swift
+// PingOrchestrate
+//
+// Copyright (c) 2024 Ping Identity. 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 PingLogger
+
+/// Enum representing the mode of module override.
+public enum OverrideMode {
+ case override // Override the previous registered module
+ case append // Append to the list, and cannot be overridden
+ case ignore // Ignore if the module is already registered
+}
+
+
+/// Workflow configuration
+public class WorkflowConfig {
+ /// Use a list instead of a map to allow registering a module twice with different configurations
+ public private(set) var modules: [any ModuleRegistryProtocol] = []
+ /// Timeout for the HTTP client, default is 15 seconds
+ public var timeout: TimeInterval = 15.0
+ /// Logger for the log, default is NoneLogger
+ public var logger: Logger = LogManager.logger {
+ didSet {
+ // Propagate the logger to Modules
+ LogManager.logger = logger
+ }
+ }
+ /// HTTP client for the engine
+ public internal(set) var httpClient: HttpClient = HttpClient()
+
+ /// Initializes a new WorkflowConfig instance.
+ public init() {}
+
+ /// Register a module.
+ /// - Parameters:
+ /// - module: The module to be registered.
+ /// - priority: The priority of the module in the registry. Default is 10.
+ /// - mode: The mode of the module registration. Default is `override`. If the mode is `override`, the module will be overridden if it is already registered.
+ /// - config: The configuration for the module.
+ public func module(_ module: Module,
+ _ priority: Int = 10,
+ mode: OverrideMode = .override,
+ _ config: @escaping (T) -> (Void) = { _ in }) {
+
+ switch mode {
+ case .override:
+
+ if let index = modules.firstIndex(where: { $0.id == module.id }) {
+ modules[index] = ModuleRegistry(setup: module.setup, priority: modules[index].priority, id: modules[index].id, config: configValue(initalValue: module.config, nextValue: config))
+ } else {
+ let registry = ModuleRegistry(setup: module.setup, priority: priority, id: module.id, config: configValue(initalValue: module.config, nextValue: config))
+ modules.append(registry )
+ }
+
+ case .append:
+ let uuid = UUID()
+ let moduleCopy = module
+ let registry = ModuleRegistry(setup: moduleCopy.setup, priority: priority, id: uuid, config: configValue(initalValue: moduleCopy.config, nextValue: config))
+ modules.append(registry)
+
+ case .ignore:
+ if modules.contains(where: { $0.id == module.id }) {
+ return
+ }
+ let registry = ModuleRegistry(setup: module.setup, priority: priority, id: module.id, config: configValue(initalValue: module.config, nextValue: config))
+ modules.append(registry)
+ }
+ }
+
+ private func configValue(initalValue: @escaping () -> (T), nextValue: @escaping (T) -> (Void)) -> T {
+ let initConfig = initalValue()
+ nextValue(initConfig)
+ return initConfig
+ }
+
+ /// Registers the workflow
+ /// - Parameter workflow: The workflow to be registered.
+ public func register(workflow: Workflow) {
+ httpClient.timeoutIntervalForRequest = timeout
+ modules.sort(by: { $0.priority < $1.priority })
+ modules.forEach { $0.register(workflow: workflow) }
+ }
+}
diff --git a/Orchestrate/OrchestrateTests/CookieModuleTests.swift b/Orchestrate/OrchestrateTests/CookieModuleTests.swift
new file mode 100644
index 0000000..f0709df
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/CookieModuleTests.swift
@@ -0,0 +1,202 @@
+//
+// CookieModuleTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingStorage
+@testable import PingOrchestrate
+
+final class CookieModuleTests: XCTestCase {
+
+ override func setUp() {
+ super.setUp()
+ MockURLProtocol.startInterceptingRequests()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ MockURLProtocol.stopInterceptingRequests()
+ }
+
+ func testCookieFromResponse() async {
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: URL(string: "http://openam.example.com")!, statusCode: 200, httpVersion: nil, headerFields: [
+ "Set-Cookie":
+ "interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com, interactionToken=abc; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com"
+ ])!, Data())
+ }
+
+ let dummy = Module.of({CustomHeaderConfig()}) { module in
+ module.transform {_,_ in
+ return SuccessNode(session: EmptySession())
+ }
+ }
+ let memory = MemoryStorage<[CustomHTTPCookie]>()
+ let workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(dummy)
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = memory
+ cookieValue.persist = ["interactionId", "interactionToken"]
+ }
+ }
+
+ _ = await workflow.start()
+ let cookies = try? await memory.get()
+ XCTAssertNotNil(cookies)
+ XCTAssertEqual(cookies?.count, 2)
+ }
+
+ func testCookieStorageFromResponse() async {
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: URL(string: "http://openam.example.com")!, statusCode: 200, httpVersion: nil, headerFields: [
+ "Set-Cookie":
+ "interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=.openam.example.com, interactionToken=abc; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=.openam.example.com"
+ ])!, Data())
+ }
+
+ let dummy = Module.of({CustomHeaderConfig()}) { module in
+ module.transform {_,_ in
+ return SuccessNode(session: EmptySession())
+ }
+ }
+ let memory = MemoryStorage<[CustomHTTPCookie]>()
+ let workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(dummy)
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = memory
+ cookieValue.persist = ["interactionId"]
+ }
+ }
+
+ _ = await workflow.start()
+ let cookies = try? await memory.get()
+ XCTAssertNotNil(cookies)
+ XCTAssertEqual(cookies?.count, 1)
+ }
+
+ func testCookieInjectToRequestAndSignoff() async {
+ var success = false
+ let json: [String: Any] = ["booleanKey": true]
+
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: URL(string: "http://openam.example.com")!, statusCode: 200, httpVersion: nil, headerFields: [
+ "Set-Cookie":
+ "interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com, interactionToken=abc; Max-Age=3600; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com"
+ ])!, Data())
+ }
+
+ var workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ }
+
+ let dummy = Module.of({CustomHeaderConfig()}) { module in
+ module.transform { flowContext, _ in
+ if success {
+ return SuccessNode(session: EmptySession())
+ } else {
+ success = true
+ return TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: [])
+ }
+ }
+ }
+ let memory = MemoryStorage<[CustomHTTPCookie]>()
+ workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(dummy)
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = memory
+ cookieValue.persist = ["interactionId"]
+ }
+ }
+
+ let node = await workflow.start()
+ _ = await (node as? ContinueNode)?.next()
+
+ XCTAssertTrue(MockURLProtocol.requestHistory[1].allHTTPHeaderFields!["Cookie"]!.contains("interactionId=178ce234-afd2-4207-984e-bda28bd7042c"))
+ XCTAssertTrue(MockURLProtocol.requestHistory[1].allHTTPHeaderFields!["Cookie"]!.contains("interactionToken=abc"))
+
+ let cookies = try? await memory.get()
+ XCTAssertNotNil(cookies)
+ XCTAssertEqual(cookies?.count, 1)
+
+ let result = await workflow.signOff()
+ switch result {
+ case .success:
+ break
+ default:
+ XCTFail("Should have succeeded")
+ }
+
+// XCTAssertTrue(MockURLProtocol.requestHistory[2].allHTTPHeaderFields!["Cookie"]!.contains("interactionId=178ce234-afd2-4207-984e-bda28bd7042c"))
+// XCTAssertFalse(MockURLProtocol.requestHistory[2].allHTTPHeaderFields!["Cookie"]!.contains("interactionToken=abc"))
+ let cookies2 = try? await memory.get()
+ XCTAssertNil(cookies2)
+
+ }
+
+ func testExpiredCookieFromResponse() async {
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: URL(string: "http://openam.example.com")!, statusCode: 200, httpVersion: nil, headerFields: [
+ "Set-Cookie":
+ "interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Path=/; Expires=Wed, 21 Oct 1999 01:00:00 GMT; HttpOnly; Domain=openam.example.com, interactionToken=abc; Path=/; Expires=Thu, 09 May 9999 21:38:44 GMT; HttpOnly; Domain=openam.example.com"
+ ])!, Data())
+ }
+
+ let dummy = Module.of({CustomHeaderConfig()}) { module in
+ module.transform {_,_ in
+ return SuccessNode(session: EmptySession())
+ }
+ }
+ let memory = MemoryStorage<[CustomHTTPCookie]>()
+ let workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(dummy)
+
+ config.module(CookieModule.config) { cookieValue in
+ cookieValue.cookieStorage = memory
+ cookieValue.persist = ["interactionId", "interactionToken"]
+ }
+
+ }
+
+ _ = await workflow.start()
+ let cookies = try? await memory.get()
+ XCTAssertNotNil(cookies)
+ XCTAssertEqual(cookies?.count, 1)
+ }
+
+
+ func testCookieIsExpiredValidationExpired() {
+ let setCookie: [String: String] = ["Set-Cookie":"iPlanetDirectoryPro=token; Expires=Wed, 21 Oct 1999 01:00:00 GMT; Domain=openam.example.com"]
+ let cookies = HTTPCookie.cookies(withResponseHeaderFields: setCookie, for: URL(string: "https://openam.example.com")!)
+ guard let cookie = cookies.first else {
+ XCTFail("Failed to parse Cookies from response header")
+ return
+ }
+ XCTAssertTrue(cookie.isExpired)
+ }
+
+
+ func testCookieIsExpiredValidationNotExpired() {
+ let setCookie: [String: String] = ["Set-Cookie":"iPlanetDirectoryPro=token; Expires=Wed, 21 Oct 2032 01:00:00 GMT; Domain=openam.example.com"]
+ let cookies = HTTPCookie.cookies(withResponseHeaderFields: setCookie, for: URL(string: "https://openam.example.com")!)
+ guard let cookie = cookies.first else {
+ XCTFail("Failed to parse Cookies from response header")
+ return
+ }
+ XCTAssertFalse(cookie.isExpired)
+ }
+}
diff --git a/Orchestrate/OrchestrateTests/CustomHeaderModuleTests.swift b/Orchestrate/OrchestrateTests/CustomHeaderModuleTests.swift
new file mode 100644
index 0000000..d98a53e
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/CustomHeaderModuleTests.swift
@@ -0,0 +1,48 @@
+//
+// CustomHeaderModuleTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingOrchestrate
+
+final class CustomHeaderModuleTest: XCTestCase {
+
+ override func setUp() {
+ super.setUp()
+ MockURLProtocol.startInterceptingRequests()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ MockURLProtocol.stopInterceptingRequests()
+ }
+
+ func testCustomHeaderAddedToRequest() async {
+ MockURLProtocol.requestHandler = { request in
+
+ XCTAssertNotNil(request.allHTTPHeaderFields?["X-Custom-Header"])
+ XCTAssertEqual("CustomValue", request.allHTTPHeaderFields?["X-Custom-Header"])
+
+ return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data())
+
+ }
+
+ let workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(CustomHeader.config) { customHeaderConfig in
+ customHeaderConfig.header(name: "X-Custom-Header", value: "CustomValue")
+
+ }
+ }
+
+ _ = await workflow.start()
+ }
+}
diff --git a/Orchestrate/OrchestrateTests/FlowContextTests.swift b/Orchestrate/OrchestrateTests/FlowContextTests.swift
new file mode 100644
index 0000000..6b332b0
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/FlowContextTests.swift
@@ -0,0 +1,41 @@
+//
+// FlowContextTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingOrchestrate
+
+final class FlowContextTests: XCTestCase {
+
+ func testInit() async throws {
+ let context = FlowContext(flowContext: SharedContext(["cookie": "pingCookie"]))
+ XCTAssertNotNil(context)
+ }
+
+ func testDefaultValues() async throws {
+ let context = FlowContext(flowContext: SharedContext(["cookie": "pingCookie"]))
+ let request = Request()
+ let body = ["bodykey": "bodyvalue"]
+ request.url("https://pingone.com")
+ request.header(name: "testHeader", value: "testValue")
+ request.header(name: "testHeader1", value: "testValue2")
+ request.header(name: "testHeader1", value: "testValue2")
+ request.parameter(name: "key1", value: "key1Value")
+ request.parameter(name: "key2", value: "key2Value")
+ request.body(body: body)
+
+ context.flowContext.set(key: "request", value: request)
+ let cookie = context.flowContext.get(key: "cookie")
+ XCTAssertNotNil(cookie)
+ let requestValue = context.flowContext.get(key: "request")
+ XCTAssertTrue(requestValue != nil)
+ XCTAssertTrue(requestValue is Request)
+ }
+}
diff --git a/Orchestrate/OrchestrateTests/ModuleTests.swift b/Orchestrate/OrchestrateTests/ModuleTests.swift
new file mode 100644
index 0000000..def5870
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/ModuleTests.swift
@@ -0,0 +1,46 @@
+//
+// ModuleTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingOrchestrate
+
+final class ModuleTests: XCTestCase {
+
+ func testModule() async throws {
+
+ class CustomHeaderConfig: Equatable {
+ var enable = true
+ var headerValue = "iOS-SDK"
+ var headerName = "header-name"
+
+ static func == (lhs: CustomHeaderConfig, rhs: CustomHeaderConfig) -> Bool {
+ return lhs.enable == rhs.enable && lhs.headerValue == rhs.headerValue && lhs.headerName == rhs.headerName
+ }
+ }
+
+ func headerconfig(setup: Setup) {
+ let config = setup.config
+ setup.next { (context, _, request) in
+ if config.enable {
+ request.header(name: config.headerName, value: config.headerValue)
+ }
+ return request
+ }
+ }
+
+ let block = { CustomHeaderConfig() }
+ let module = Module.of(block, setup: headerconfig)
+
+ XCTAssertNotNil(module.config)
+ XCTAssertNotNil(module.setup)
+
+ }
+}
diff --git a/Orchestrate/OrchestrateTests/NodeTests.swift b/Orchestrate/OrchestrateTests/NodeTests.swift
new file mode 100644
index 0000000..e8535ad
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/NodeTests.swift
@@ -0,0 +1,66 @@
+//
+// NodeTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingOrchestrate
+
+final class NodeTests: XCTestCase {
+
+ func testConnectorNextShouldReturnContinueNodeInWorkflow() async {
+ let mockWorkflow = WorkflowMock(config: WorkflowConfig())
+ let mockContext = FlowContextMock(flowContext: SharedContext())
+ let mockNode = NodeMock()
+
+ mockWorkflow.nextReturnValue = mockNode
+
+ let connector = TestContinueNode(context: mockContext, workflow: mockWorkflow, input: [:], actions: [])
+
+ let continueNode = await connector.next()
+ XCTAssertTrue(continueNode as? NodeMock === mockNode)
+ }
+
+ func testConnectorCloseShouldCloseAllCloseableActions() {
+ let closeableAction = TestAction()
+ let connector = TestContinueNode(context: FlowContextMock(flowContext: SharedContext()), workflow: WorkflowMock(config: WorkflowConfig()), input: [:], actions: [closeableAction])
+
+ connector.close()
+
+ XCTAssertTrue(closeableAction.isClosed)
+ }
+}
+
+// Supporting Test Classes
+class WorkflowMock: Workflow {
+ var nextReturnValue: Node?
+ override func next(_ context: FlowContext, _ current: ContinueNode) async -> Node {
+ return nextReturnValue ?? NodeMock()
+ }
+}
+
+class FlowContextMock: FlowContext {}
+
+class NodeMock: Node {}
+
+class TestContinueNode: ContinueNode {
+ override func asRequest() -> Request {
+ return RequestMock(urlString: "https://openam.example.com")
+ }
+}
+
+class TestAction: Action, Closeable {
+ var isClosed = false
+ func close() {
+ isClosed = true
+ }
+}
+
+class RequestMock: Request {}
diff --git a/Orchestrate/OrchestrateTests/OrchestrateTests.swift b/Orchestrate/OrchestrateTests/OrchestrateTests.swift
new file mode 100644
index 0000000..b54daf9
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/OrchestrateTests.swift
@@ -0,0 +1,252 @@
+//
+// OrchestrateTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+
+//
+//import Testing
+//
+//@testable import PingOrchestrate
+//
+//struct OrchestrateTests {
+//
+// @Test func testWorkFlowOverridable() async throws {
+//
+// class CustomHeaderConfig {
+// var enable = true
+// var headerValue = "iOS-SDK"
+// var headerName = "header-name"
+// }
+//
+// let customHeader = Module.of({ CustomHeaderConfig() }, block: { setup in
+// let config = setup.config
+// setup.next { ( context, _, request) in
+// if config.enable {
+// request.header(name: config.headerName, value: config.headerValue)
+// }
+// return request
+// }
+//
+// setup.start { ( context, request) in
+// if config.enable {
+// request.header(name: config.headerName, value: config.headerValue)
+// }
+// return request
+// }
+//
+// })
+//
+//
+// let nosession = Module.of { setup in
+// setup.next { ( context,_, request) in
+// request.header(name: "nosession", value: "true")
+// return request
+// }
+// }
+//
+//
+// let forceAuth = Module.of { setup in
+// setup.start { ( context, request) in
+// request.header(name: "forceAuth", value: "true")
+// return request
+// }
+// }
+//
+// let workFlow = Workflow.config { config in
+// config.debug = true
+// config.timeout = 10
+//
+// config.module(customHeader) { header in
+// header.headerName = "header-name2"
+// header.headerValue = "iOS-SDK"
+// }
+//
+// config.module(customHeader) { header in
+// header.headerName = "header-name1"
+// header.headerValue = "Android-SDK"
+// }
+//
+// config.module(forceAuth)
+// config.module(nosession)
+//
+// }
+//
+// #expect(workFlow.workFlowConfig.modules.count == 3)
+//
+//
+// }
+//
+// @Test("This validates customer can be overridate later") func testWorkFlowOverridable1() async throws {
+//
+// class CustomHeaderConfig {
+// var enable = true
+// var headerValue = "iOS-SDK"
+// var headerName = "header-name"
+// }
+//
+// let customHeader = Module.of({ CustomHeaderConfig() }, block: { setup in
+// let config = setup.config
+// setup.next { ( context,_, request) in
+// if config.enable {
+// request.header(name: config.headerName, value: config.headerValue)
+// }
+// return request
+// }
+//
+// setup.start { ( context, request) in
+// if config.enable {
+// request.header(name: config.headerName, value: config.headerValue)
+// }
+// return request
+// }
+//
+// })
+//
+//
+// let nosession = Module.of { setup in
+// setup.next { ( context, _, request) in
+// request.header(name: "nosession", value: "true")
+// return request
+// }
+// }
+//
+//
+// let forceAuth = Module.of{ setup in
+// setup.start { ( context, request) in
+// request.header(name: "forceAuth", value: "true")
+// return request
+// }
+// }
+//
+// let workFlow = Workflow.config { config in
+// config.debug = true
+// config.timeout = 10
+//
+// config.module(customHeader, mode: OverrideMode.append) { header in
+// header.enable = true
+// header.headerName = "header-name1"
+// header.headerValue = "Android-SDK1"
+// }
+//
+// config.module(customHeader) { header in
+// header.enable = true
+// header.headerName = "header-name2"
+// header.headerValue = "Android-SDK2"
+// }
+//
+// config.module(customHeader, mode: OverrideMode.append) { header in
+// header.enable = true
+// header.headerName = "header-name3"
+// header.headerValue = "iOS-SDK3"
+// }
+//
+//
+// config.module(customHeader) { header in
+// header.enable = true
+// header.headerName = "header-name4"
+// header.headerValue = "Android-SDK4"
+// }
+//
+//
+// config.module(forceAuth)
+// config.module(nosession)
+//
+// }
+//
+// try #require(workFlow.workFlowConfig.modules.count > 0)
+//
+// #expect(workFlow.workFlowConfig.modules.count == 5)
+//
+// }
+//
+//// @Test("This validates customer can be overridate later") func testWorkFlowOverridable2() async throws {
+////
+//// class CustomHeaderConfig {
+//// var enable = true
+//// var headerValue = "iOS-SDK"
+//// var headerName = "header-name"
+//// }
+////
+//// let customHeader = Module.of({ CustomHeaderConfig()}, priority: .low, block: { setup in
+//// let config = setup.config
+//// setup.next { ( context,_, request) in
+//// if config.enable {
+//// request.header(name: config.headerName, value: config.headerValue)
+//// }
+//// return request
+//// }
+////
+//// setup.start { ( context, request) in
+//// if config.enable {
+//// request.header(name: config.headerName, value: config.headerValue)
+//// }
+//// return request
+//// }
+////
+//// })
+////
+////
+//// let nosession = Module.of(priority: .high) { setup in
+//// setup.next { ( context, _, request) in
+//// request.header(name: "nosession", value: "true")
+//// return request
+//// }
+//// }
+////
+////
+//// let forceAuth = Module.of{ setup in
+//// setup.start { ( context, request) in
+//// request.header(name: "forceAuth", value: "true")
+//// return request
+//// }
+//// }
+////
+//// let workFlow = Workflow.config { config in
+//// config.debug = true
+//// config.timeout = 10
+////
+//// config.module(customHeader, false) { header in
+//// header.enable = true
+//// header.headerName = "header-name1"
+//// header.headerValue = "Android-SDK1"
+//// }
+////
+//// config.module(customHeader) { header in
+//// header.enable = true
+//// header.headerName = "header-name2"
+//// header.headerValue = "Android-SDK2"
+//// }
+////
+//// config.module(customHeader, false) { header in
+//// header.enable = true
+//// header.headerName = "header-name3"
+//// header.headerValue = "iOS-SDK3"
+//// }
+////
+////
+//// config.module(customHeader) { header in
+//// header.enable = true
+//// header.headerName = "header-name4"
+//// header.headerValue = "Android-SDK4"
+//// }
+////
+////
+//// config.module(forceAuth)
+//// config.module(nosession)
+////
+//// }
+////
+//// try #require(workFlow.workFlowConfig.modules.count > 0)
+////
+//// #expect(workFlow.workFlowConfig.modules.count == 5)
+//// #expect(workFlow.workFlowConfig.highPriorityModule.count == 1)
+//// #expect(workFlow.workFlowConfig.lowPriorityModule.count == 4)
+////
+//// }
+////
+//
+//}
diff --git a/Orchestrate/OrchestrateTests/RequestTests.swift b/Orchestrate/OrchestrateTests/RequestTests.swift
new file mode 100644
index 0000000..5c18f10
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/RequestTests.swift
@@ -0,0 +1,100 @@
+//
+// RequestTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingOrchestrate
+
+final class RequestTests: XCTestCase {
+
+ func testInit() async throws {
+ let request = Request()
+ XCTAssertNotNil(request)
+ }
+
+ func testDefaultValues() async throws {
+ let request = Request()
+ let body = ["bodykey": "bodyvalue"]
+ let json = try? JSONSerialization.data(withJSONObject: body, options: [])
+ request.url("https://pingone.com")
+ request.header(name: "testHeader", value: "testValue")
+ request.header(name: "testHeader1", value: "testValue2")
+ request.header(name: "testHeader1", value: "testValue2")
+ request.parameter(name: "key1", value: "key1Value")
+ request.parameter(name: "key2", value: "key2Value")
+ request.body(body: body)
+ XCTAssertTrue(request.urlRequest.httpBody == json)
+ XCTAssertTrue(request.urlRequest.allHTTPHeaderFields?.count == 3)
+ XCTAssertTrue(request.urlRequest.url?.absoluteString == "https://pingone.com?key1=key1Value&key2=key2Value")
+ }
+
+ func testDefaultValuesFormUrlEncoded() async throws {
+ let request = Request()
+ let body = ["bodykey": "bodyvalue"]
+ let jsonData = "bodykey=bodyvalue".data(using: .utf8)
+ request.url("https://pingone.com")
+ request.header(name: "testHeader", value: "testValue")
+ request.header(name: "testHeader1", value: "testValue2")
+ request.header(name: "testHeader1", value: "testValue2")
+ request.parameter(name: "key1", value: "key1Value")
+ request.parameter(name: "key2", value: "key2Value")
+ request.form(formData: body)
+ XCTAssertTrue(request.urlRequest.httpBody == jsonData)
+ XCTAssertTrue(request.urlRequest.allHTTPHeaderFields?.count == 3)
+ XCTAssertTrue(request.urlRequest.url?.absoluteString == "https://pingone.com?key1=key1Value&key2=key2Value")
+ }
+
+ func testUrlSetsTheCorrectUrl() {
+ let request = Request()
+ request.url("http://example.com")
+ XCTAssertEqual("http://example.com", request.urlRequest.url?.absoluteString)
+ }
+
+ func testParameterAppendsTheCorrectParameter() {
+ let request = Request()
+ request.parameter(name: "key", value: "value")
+ XCTAssertEqual("value", request.urlRequest.url?.query?.components(separatedBy: "&").first(where: { $0.contains("key=") })?.split(separator: "=")[1])
+ }
+
+ func testHeaderAppendsTheCorrectHeader() {
+ let request = Request()
+ request.header(name: "Content-Type", value: "application/json")
+ XCTAssertEqual("application/json", request.urlRequest.allHTTPHeaderFields?["Content-Type"])
+ }
+
+ func testCookiesSetsTheCorrectCookies() {
+ let request = Request()
+ let setCookie: [String: String] = ["Set-Cookie":"interactionId=178ce234-afd2-4207-984e-bda28bd7042c; Max-Age=3600; Path=/; Expires=Thu, 09 May 2024 21:38:44 GMT; HttpOnly; Secure, interactionToken=abc; Max-Age=3600; Path=/; Expires=Thu, 09 May 2024 21:38:44 GMT; HttpOnly; Secure"]
+ let httpCookies = HTTPCookie.cookies(withResponseHeaderFields: setCookie, for: URL(string: "https://openam.example.com")!)
+ request.cookies(cookies: httpCookies)
+
+ let cookieHeader = request.urlRequest.allHTTPHeaderFields?["Cookie"] ?? ""
+ XCTAssertTrue(cookieHeader.contains("interactionId"))
+ XCTAssertTrue(cookieHeader.contains("interactionToken"))
+ }
+
+ func testBodySetsTheCorrectBody() {
+ let request = Request()
+ let jsonData = try! JSONSerialization.data(withJSONObject: ["key": "value"], options: [])
+ let body = try! JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any]
+ request.body(body: body)
+ XCTAssertEqual(String(data: request.urlRequest.httpBody!, encoding: .utf8), """
+ {"key":"value"}
+ """)
+ }
+
+ func testFormSetsTheCorrectFormData() {
+ let request = Request()
+ let body = ["key": "value"]
+ request.form(formData: body)
+ XCTAssertEqual(String(data: request.urlRequest.httpBody!, encoding: .utf8), "key=value")
+ }
+}
diff --git a/Orchestrate/OrchestrateTests/ResponseTests.swift b/Orchestrate/OrchestrateTests/ResponseTests.swift
new file mode 100644
index 0000000..4ef4054
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/ResponseTests.swift
@@ -0,0 +1,93 @@
+//
+// ResponseTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingOrchestrate
+
+final class ResponseTests: XCTestCase {
+
+ func testInit() async throws {
+ let response = Response(data: Data(), response: URLResponse())
+ XCTAssertNotNil(response.data)
+ XCTAssertNotNil(response.response)
+ }
+
+ func testDefaultValues() async throws {
+ let data = Data("{}".utf8)
+ let response = Response(data: data, response: URLResponse())
+ XCTAssertNotNil(response.body())
+ XCTAssertNotNil(try response.json(data: data))
+ }
+
+ func testHeader() async throws {
+ let data = Data("{}".utf8)
+
+ let setCookie: [String: String] = ["Set-Cookie":"Ping=token; Expires=Wed, 21 Oct 1999 01:00:00 GMT; Domain=openam.example.com"]
+
+ let urlresponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: setCookie)!
+
+ let response = Response(data: data, response: urlresponse)
+
+ XCTAssertEqual(response.header(name: "Set-Cookie"), "Ping=token; Expires=Wed, 21 Oct 1999 01:00:00 GMT; Domain=openam.example.com")
+
+ XCTAssertEqual(response.status(), 200)
+ XCTAssertNotNil(try response.json(data: data))
+
+ XCTAssertEqual(response.getCookies().count, 1)
+
+ }
+
+ func testBodyShouldReturnResponseBodyAsString() {
+ let responseBody = "response body".data(using: .utf8)!
+ let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: nil)!
+ let response = Response(data: responseBody, response: urlResponse)
+
+ XCTAssertEqual(response.body(), "response body")
+ }
+
+ func testStatusShouldReturnResponseStatusCode() {
+ let responseBody = "response body".data(using: .utf8)!
+ let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: nil)!
+ let response = Response(data: responseBody, response: urlResponse)
+
+ XCTAssertEqual(response.status(), 200)
+ }
+
+ func testCookiesShouldReturnCookiesFromResponse() {
+ let responseBody = "response body".data(using: .utf8)!
+ let setCookie: [String: String] = ["Set-Cookie":"cookie1=value1, cookie2=value2"]
+ let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: setCookie)!
+ let response = Response(data: responseBody, response: urlResponse)
+
+ XCTAssertEqual(response.getCookies().count, 2)
+ XCTAssertEqual(response.getCookies().first?.name, "cookie1")
+ XCTAssertEqual(response.getCookies().first?.value, "value1")
+ XCTAssertEqual(response.getCookies().last?.name, "cookie2")
+ XCTAssertEqual(response.getCookies().last?.value, "value2")
+ XCTAssertEqual(response.header(name: "Set-Cookie"), "cookie1=value1, cookie2=value2")
+ }
+
+ func testHeaderShouldReturnSpecificHeaderValue() {
+ let responseBody = "response body".data(using: .utf8)!
+ let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: ["Content-Type": "application/json"])!
+ let response = Response(data: responseBody, response: urlResponse)
+
+ XCTAssertEqual(response.header(name: "Content-Type"), "application/json")
+ }
+
+ func testHeaderShouldReturnNullIfHeaderIsNotPresent() {
+ let responseBody = "response body".data(using: .utf8)!
+ let urlResponse = HTTPURLResponse(url: URL(string: "https://ping.com")! , statusCode: 200, httpVersion: "1.0", headerFields: nil)!
+ let response = Response(data: responseBody, response: urlResponse)
+
+ XCTAssertNil(response.header(name: "Non-Existent-Header"))
+ }
+}
diff --git a/Orchestrate/OrchestrateTests/SessionTests.swift b/Orchestrate/OrchestrateTests/SessionTests.swift
new file mode 100644
index 0000000..293f933
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/SessionTests.swift
@@ -0,0 +1,31 @@
+//
+// SessionTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingOrchestrate
+
+final class SessionTests: XCTestCase {
+
+ func testEmptySessionValueShouldReturnEmptyString() {
+ XCTAssertEqual("", EmptySession().value)
+ }
+
+ func testSessionValueShouldReturnCorrectSessionValue() {
+ let session = MockSession()
+ XCTAssertEqual("session_value", session.value)
+ }
+
+ class MockSession: Session {
+ var value: String = "session_value"
+ }
+
+}
diff --git a/Orchestrate/OrchestrateTests/WorkflowTests.swift b/Orchestrate/OrchestrateTests/WorkflowTests.swift
new file mode 100644
index 0000000..ecceea4
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/WorkflowTests.swift
@@ -0,0 +1,666 @@
+//
+// WorkflowTests.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingOrchestrate
+
+class CustomHeaderConfig {
+ var enable = true
+ var headerValue = "iOS-SDK"
+ var headerName = "header-name"
+}
+
+class WorkflowTest: XCTestCase {
+
+ var customHeader = Module.of({ CustomHeaderConfig() }, setup: { setup in
+ let config = setup.config
+ setup.next { ( context, _, request) in
+ if config.enable {
+ request.header(name: config.headerName, value: config.headerValue)
+ }
+ return request
+ }
+
+ setup.start { ( context, request) in
+ if config.enable {
+ request.header(name: config.headerName, value: config.headerValue)
+ }
+ return request
+ }
+ })
+
+ let nosession = Module.of { setup in
+ setup.next { ( context,_, request) in
+ request.header(name: "nosession", value: "true")
+ return request
+ }
+ }
+
+
+ let forceAuth = Module.of { setup in
+ setup.start { ( context, request) in
+ request.header(name: "forceAuth", value: "true")
+ return request
+ }
+ }
+
+ override func setUp() {
+ super.setUp()
+ MockURLProtocol.startInterceptingRequests()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ MockURLProtocol.stopInterceptingRequests()
+ }
+
+ func testSameModuleInstanceShouldOverrideExistingModule() {
+
+ let workflow = Workflow.createWorkflow { config in
+ config.timeout = 10
+
+ config.module(forceAuth)
+
+ config.module(customHeader) { header in
+ header.headerName = "header-name1"
+ header.headerValue = "Android-SDK"
+ }
+
+ config.module(nosession)
+
+ config.module(customHeader) { header in
+ header.headerName = "header-name2"
+ header.headerValue = "iOS-SDK"
+ }
+ }
+
+ XCTAssertEqual(workflow.config.modules.count, 3)
+ let moduleRegistry = workflow.config.modules.first(where: { $0.id == customHeader.id })
+ XCTAssertEqual((moduleRegistry?.config as? CustomHeaderConfig)?.headerValue, "iOS-SDK")
+ XCTAssertEqual(forceAuth.id, workflow.config.modules[0].id)
+ XCTAssertEqual(customHeader.id, workflow.config.modules[1].id)
+ XCTAssertEqual(nosession.id, workflow.config.modules[2].id)
+
+ }
+
+ func testSameModuleInstanceShouldOverrideExistingModuleWithPriority (){
+ let workflow = Workflow.createWorkflow { config in
+ config.timeout = 10
+
+ config.module(forceAuth, 1)
+
+ config.module(customHeader, 2) { header in
+ header.headerName = "header-name1"
+ header.headerValue = "Android-SDK"
+ }
+
+ config.module(nosession, 3)
+
+ config.module(customHeader, 10) { header in
+ header.headerName = "header-name2"
+ header.headerValue = "iOS-SDK"
+ }
+ }
+
+ XCTAssertEqual(workflow.config.modules.count, 3)
+ let moduleRegistry = workflow.config.modules.first(where: { $0.id == customHeader.id })
+ XCTAssertEqual((moduleRegistry?.config as? CustomHeaderConfig)?.headerValue, "iOS-SDK")
+ let index = workflow.config.modules.firstIndex(where: { $0.id == customHeader.id })
+ //The original position is retained even with the new priority
+ XCTAssertEqual(index, 1)
+
+ }
+
+ func testNotOverrideExistingModule() {
+ let workflow = Workflow.createWorkflow { config in
+ config.timeout = 10
+
+ config.module(forceAuth)
+ config.module(nosession)
+
+ config.module(customHeader) { header in
+ header.headerName = "header-name1"
+ header.headerValue = "Android-SDK"
+ }
+ // You cannot register 2 modules with the same instance,
+ // the later one will replace the previous one
+ // To register the same module twice, you need to add overridable = false
+ // APPEND means the module cannot be replaced and does not replace previous module
+ config.module(customHeader, mode: .append) { header in
+ header.headerName = "header-name2"
+ header.headerValue = "iOS-SDK"
+ }
+ }
+
+ XCTAssertEqual(workflow.config.modules.count, 4)
+ }
+
+ func testNotOverrideExistingModuleWithIgnore() {
+ let workflow = Workflow.createWorkflow { config in
+ config.timeout = 10
+
+ config.module(forceAuth)
+ config.module(nosession)
+
+ config.module(customHeader) { header in
+ header.headerName = "header-name1"
+ header.headerValue = "Android-SDK"
+ }
+ config.module(customHeader, mode: .ignore) { header in
+ header.headerName = "header-name2"
+ header.headerValue = "iOS-SDK"
+ }
+ }
+
+ XCTAssertEqual(workflow.config.modules.count, 3)
+ let moduleRegistry = workflow.config.modules.first(where: { $0.id == customHeader.id })
+ XCTAssertEqual((moduleRegistry?.config as? CustomHeaderConfig)?.headerValue, "Android-SDK")
+ }
+
+ func testAddModuleWithIgnore() {
+ let workflow = Workflow.createWorkflow { config in
+ config.timeout = 10
+
+ config.module(forceAuth)
+ config.module(nosession)
+
+ config.module(customHeader, mode: .ignore) { header in
+ header.headerName = "header-name2"
+ header.headerValue = "iOS-SDK"
+ }
+ }
+
+ XCTAssertEqual(workflow.config.modules.count, 3)
+ let moduleRegistry = workflow.config.modules.first(where: { $0.id == customHeader.id })
+ XCTAssertEqual((moduleRegistry?.config as? CustomHeaderConfig)?.headerValue, "iOS-SDK")
+ }
+
+ func testModuleDefaultPriority() {
+ let workflow = Workflow.createWorkflow { config in
+ config.timeout = 10
+
+ config.module(forceAuth)
+ config.module(nosession)
+
+ config.module(customHeader) { header in
+ header.headerName = "header-name2"
+ header.headerValue = "iOS-SDK"
+ }
+
+ config.module(nosession)
+ }
+
+ XCTAssertEqual(workflow.config.modules.count, 3)
+ XCTAssertEqual(forceAuth.id, workflow.config.modules[0].id)
+ XCTAssertEqual(nosession.id, workflow.config.modules[1].id)
+ XCTAssertEqual(customHeader.id, workflow.config.modules[2].id)
+ }
+
+ func testModuleCustomPriority() {
+ let workflow = Workflow.createWorkflow { config in
+ config.timeout = 10
+
+ config.module(forceAuth, 2)
+ config.module(nosession, 3)
+
+ config.module(customHeader, 1) { header in
+ header.headerName = "header-name2"
+ header.headerValue = "iOS-SDK"
+ }
+ }
+
+ XCTAssertEqual(workflow.config.modules.count, 3)
+ XCTAssertEqual(customHeader.id, workflow.config.modules[0].id)
+ XCTAssertEqual(forceAuth.id, workflow.config.modules[1].id)
+ XCTAssertEqual(nosession.id, workflow.config.modules[2].id)
+ }
+
+ func testModuleWithSamePriority() {
+ let workflow = Workflow.createWorkflow { config in
+ config.timeout = 10
+
+ config.module(forceAuth, 2)
+ config.module(nosession, 2)
+
+ config.module(customHeader, 1) { header in
+ header.headerName = "header-name2"
+ header.headerValue = "iOS-SDK"
+ }
+
+ }
+
+ XCTAssertEqual(workflow.config.modules.count, 3)
+ XCTAssertEqual(customHeader.id, workflow.config.modules[0].id)
+ XCTAssertEqual(forceAuth.id, workflow.config.modules[1].id)
+ XCTAssertEqual(nosession.id, workflow.config.modules[2].id)
+ }
+
+ func testModuleRegistrationInEachStage() {
+ let workflow = Workflow(config: WorkflowConfig())
+ XCTAssertTrue(workflow.config.modules.isEmpty)
+ XCTAssertTrue(workflow.initHandlers.isEmpty)
+ XCTAssertTrue(workflow.startHandlers.isEmpty)
+ XCTAssertTrue(workflow.nextHandlers.isEmpty)
+ XCTAssertTrue(workflow.responseHandlers.isEmpty)
+ XCTAssertTrue(workflow.nodeHandlers.isEmpty)
+ XCTAssertTrue(workflow.successHandlers.isEmpty)
+ XCTAssertTrue(workflow.signOffHandlers.isEmpty)
+
+ let dummy = Module.of({CustomHeaderConfig()}) { module in
+ module.initialize {
+ // Intercept all send request and inject custom header
+ }
+ module.start { _, request in
+ return request
+ }
+ module.next { _,_, request in
+ return request
+ }
+ module.response {_,_ in
+ // Handle response
+ }
+ module.node { _, node in
+ return node
+ }
+ module.success { _, success in
+ return success
+ }
+ module.transform {_,_ in
+ return SuccessNode(session: EmptySession())
+ }
+ module.signOff { signOff in
+ return signOff
+ }
+ }
+
+ let workflow2 = Workflow.createWorkflow { config in
+ config.module(dummy)
+ }
+
+ XCTAssertEqual(workflow2.config.modules.count, 1)
+ XCTAssertEqual(workflow2.initHandlers.count, 1)
+ XCTAssertEqual(workflow2.startHandlers.count, 1)
+ XCTAssertEqual(workflow2.nextHandlers.count, 1)
+ XCTAssertEqual(workflow2.responseHandlers.count, 1)
+ XCTAssertEqual(workflow2.nodeHandlers.count, 1)
+ XCTAssertEqual(workflow2.successHandlers.count, 1)
+ XCTAssertEqual(workflow2.signOffHandlers.count, 1)
+ }
+
+ func testModuleExecution() async {
+ var initializeCnt = 0
+ var startCnt = 0
+ var nextCnt = 0
+ var responseCnt = 0
+ var transformCnt = 0
+ var nodeReceivedCnt = 0
+ var successCnt = 0
+ var success = false
+
+ let json: [String: Bool] = ["booleanKey": true]
+
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data())
+ }
+
+ var workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ }
+
+ let dummy = Module.of({CustomHeaderConfig()}) { module in
+ module.initialize {
+ initializeCnt += 1
+ }
+ module.start { _, request in
+ startCnt += 1
+ return request
+ }
+ module.next { _,_, request in
+ nextCnt += 1
+ return request
+ }
+ module.response {_,_ in
+ responseCnt += 1
+ // Handle response
+ }
+ module.node { _, node in
+ nodeReceivedCnt += 1
+ return node
+ }
+ module.success { _, success1 in
+ successCnt += 1
+ return success1
+ }
+ module.transform { flowContext,_ in
+ transformCnt += 1
+ if success {
+ return SuccessNode(session: EmptySession())
+ } else {
+ success = true
+ return TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: [])
+ }
+ }
+ }
+
+ workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(dummy)
+ }
+
+ let node = await workflow.start()
+ let connector = node as! ContinueNode
+ XCTAssertTrue(workflow === connector.workflow)
+ XCTAssertEqual(json, connector.input as? [String: Bool])
+ XCTAssertTrue(connector.actions.isEmpty)
+ let isEmpty = connector.context.flowContext.isEmpty
+ XCTAssertTrue(isEmpty)
+ _ = await connector.next()
+ XCTAssertEqual(1, initializeCnt)
+ XCTAssertEqual(1, startCnt)
+ XCTAssertEqual(1, nextCnt)
+ XCTAssertEqual(2, responseCnt)
+ XCTAssertEqual(2, transformCnt)
+ XCTAssertEqual(2, nodeReceivedCnt)
+ XCTAssertEqual(1, successCnt)
+ }
+
+ func testAccessToWorkflowContext() async {
+ let json: [String: Any] = ["booleanKey": true]
+ var success = false
+
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data())
+ }
+
+ var workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ }
+
+ let dummy = Module.of({CustomHeaderConfig()}) { module in
+ module.initialize {
+ let count = (workflow.sharedContext.get(key: "count") as! Int) + 1
+ workflow.sharedContext.set(key: "count", value: count)
+ }
+ module.start { _, request in
+ let count = (workflow.sharedContext.get(key: "count")as! Int) + 1
+ workflow.sharedContext.set(key: "count", value: count)
+ return request
+ }
+ module.next { _,_, request in
+ let count = (workflow.sharedContext.get(key: "count")as! Int) + 1
+ workflow.sharedContext.set(key: "count", value: count)
+ return request
+ }
+ module.response {_,_ in
+ let count = (workflow.sharedContext.get(key: "count")as! Int) + 1
+ workflow.sharedContext.set(key: "count", value: count)
+ // Handle response
+ }
+ module.node { _, node in
+ let count = (workflow.sharedContext.get(key: "count")as! Int) + 1
+ workflow.sharedContext.set(key: "count", value: count)
+ return node
+ }
+ module.success { _, success1 in
+ let count = (workflow.sharedContext.get(key: "count")as! Int) + 1
+ workflow.sharedContext.set(key: "count", value: count)
+ return success1
+ }
+ module.transform { flowContext,_ in
+ let count = (workflow.sharedContext.get(key: "count")as! Int) + 1
+ workflow.sharedContext.set(key: "count", value: count)
+ if success {
+ return SuccessNode(session: EmptySession())
+ } else {
+ success = true
+ return TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: [])
+ }
+ }
+ }
+
+ workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(dummy)
+ }
+
+ workflow.sharedContext.set(key: "count", value: 0)
+
+ let node = await workflow.start()
+ _ = await (node as! ContinueNode).next()
+
+ let count = (workflow.sharedContext.get(key: "count")as! Int)
+ XCTAssertEqual(10, count)
+ }
+
+ func testInitStateFunctionThrowException() async {
+ let initFailed = Module.of({CustomHeaderConfig()}) { module in
+ module.initialize {
+ throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"])
+ }
+ }
+
+ let workflow = Workflow.createWorkflow { config in
+ config.module(initFailed)
+ }
+
+ let node = await workflow.start()
+ XCTAssertTrue(node is FailureNode)
+ XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize")
+ }
+
+ func testStartStateFunctionThrowsException() async throws {
+ let startFailed = Module.of({CustomHeaderConfig()}) { module in
+ module.start { _,_ in
+ throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"])
+ }
+ }
+ let workflow = Workflow.createWorkflow { config in
+ config.module(startFailed)
+ }
+
+ let node = await workflow.start()
+ XCTAssertTrue(node is FailureNode)
+ XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize")
+ }
+
+ func testResponseStateFunctionThrowsException() async throws {
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data())
+ }
+
+ let responseFailed = Module.of({CustomHeaderConfig()}) { module in
+ module.response { _,_ in
+ throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"])
+ }
+ }
+ let workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(responseFailed)
+ }
+
+ let node = await workflow.start()
+ XCTAssertTrue(node is FailureNode)
+ XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize")
+ }
+
+ func testTransformStateFunctionThrowsException() async throws {
+
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data())
+ }
+
+ let transformFailed = Module.of({CustomHeaderConfig()}) { module in
+ module.transform { _,_ in
+ throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"])
+ }
+ }
+ let workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(transformFailed)
+ }
+
+ let node = await workflow.start()
+ XCTAssertTrue(node is FailureNode)
+ XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize")
+ }
+
+ func testNextStateFunctionThrowsException() async throws {
+ let json = ["booleanKey": true]
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ var workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ }
+
+ let nextFailed = Module.of({CustomHeaderConfig()}) { module in
+ module.next { _,_, _ in
+ throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"])
+ }
+
+ module.transform { flowContext,_ in
+ TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: [])
+ }
+ }
+
+ workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(nextFailed)
+ }
+
+ let node = await workflow.start()
+ let next = await (node as? ContinueNode)?.next()
+ XCTAssertTrue(next is FailureNode)
+ XCTAssertEqual((next as! FailureNode).cause.localizedDescription, "Failed to initialize")
+ }
+
+ func testNodeStateFunctionThrowsException() async throws {
+ let json = ["booleanKey": true]
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ var workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ }
+
+ let nodeFailed = Module.of({CustomHeaderConfig()}) { module in
+ module.node { _, _ in
+ throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"])
+ }
+
+ module.transform { flowContext,_ in
+ TestContinueNode(context: flowContext, workflow: workflow, input: json, actions: [])
+ }
+ }
+
+ workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(nodeFailed)
+ }
+
+ let node = await workflow.start()
+ XCTAssertTrue(node is FailureNode)
+ XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize")
+ }
+
+ func testSuccessStateFunctionThrowsException() async throws {
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, Data())
+ }
+ var workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ }
+
+ let successFailed = Module.of({CustomHeaderConfig()}) { module in
+ module.success { _, _ in
+ throw NSError(domain: "InitializationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize"])
+ }
+
+ module.transform { flowContext,_ in
+ SuccessNode(session: EmptySession())
+ }
+ }
+
+ workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(successFailed)
+ }
+
+ let node = await workflow.start()
+ XCTAssertTrue(node is FailureNode)
+ XCTAssertEqual((node as! FailureNode).cause.localizedDescription, "Failed to initialize")
+ }
+
+ func testExecutionFailure() async {
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: request.url!, statusCode: 400, httpVersion: nil, headerFields: nil)!, "Invalid request".data(using: .utf8)!)
+ }
+
+ let dummy = Module.of({CustomHeaderConfig()}) { module in
+ module.transform {_,_ in
+ return ErrorNode(input: [:], message: "Invalid request")
+ }
+ }
+
+ let workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ config.module(dummy)
+ }
+
+ let node = await workflow.start()
+ let failure = node as! ErrorNode
+ XCTAssertEqual("Invalid request", failure.message)
+ XCTAssertTrue(failure.input.isEmpty)
+ }
+
+ func testSignOffShouldReturnSuccessResultWhenNoExceptionsOccur() async {
+
+ MockURLProtocol.requestHandler = { request in
+ return (HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!, "".data(using: .utf8)!)
+ }
+
+ let workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ }
+
+ let result = await workflow.signOff()
+
+ switch result {
+ case .success(let result):
+ XCTAssertNotNil(result)
+ break
+
+ default:
+ XCTFail("Expected success result")
+ }
+
+ }
+
+ func testSignOffShouldReturnFailureResultWhenExceptionOccurs() async {
+ MockURLProtocol.requestHandler = { request in
+ throw NSError(domain: "SignOffError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Sign off failed"])
+ }
+
+ let workflow = Workflow.createWorkflow { config in
+ config.httpClient = HttpClient(session: .shared)
+ }
+
+ let result = await workflow.signOff()
+ switch result {
+ case .failure(let error):
+ XCTAssertEqual(error.localizedDescription, "Sign off failed")
+ break
+
+ default:
+ XCTFail("Expected failure result")
+ }
+ }
+}
diff --git a/Orchestrate/OrchestrateTests/mock/MockAPIEndpoint.swift b/Orchestrate/OrchestrateTests/mock/MockAPIEndpoint.swift
new file mode 100644
index 0000000..0aa8910
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/mock/MockAPIEndpoint.swift
@@ -0,0 +1,43 @@
+//
+// MockAPIEndpoint.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+enum MockAPIEndpoint {
+ static let baseURL = "https://auth.test-one-pingone.com"
+
+ case authorization
+ case token
+ case userinfo
+ case endSession
+ case revocation
+ case discovery
+ case customHTMLTemplate
+
+ var url: URL {
+ switch self {
+ case .authorization:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/authorize")!
+ case .token:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/token")!
+ case .userinfo:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/userinfo")!
+ case .endSession:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/signoff")!
+ case .revocation:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/revoke")!
+ case .discovery:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/.well-known/openid-configuration")!
+ case .customHTMLTemplate:
+ return URL(string: "\(MockAPIEndpoint.baseURL)/customHTMLTemplate")!
+ }
+ }
+}
diff --git a/Orchestrate/OrchestrateTests/mock/MockURLProtocol.swift b/Orchestrate/OrchestrateTests/mock/MockURLProtocol.swift
new file mode 100644
index 0000000..23cedb6
--- /dev/null
+++ b/Orchestrate/OrchestrateTests/mock/MockURLProtocol.swift
@@ -0,0 +1,58 @@
+//
+// MockURLProtocol.swift
+// OrchestrateTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+
+class MockURLProtocol: URLProtocol {
+ public static var requestHistory: [URLRequest] = [URLRequest]()
+
+ static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
+
+ static func startInterceptingRequests() {
+ URLProtocol.registerClass(MockURLProtocol.self)
+ }
+
+ static func stopInterceptingRequests() {
+ URLProtocol.unregisterClass(MockURLProtocol.self)
+ requestHistory.removeAll()
+ }
+
+ override class func canInit(with request: URLRequest) -> Bool {
+ return true
+ }
+
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+ return request
+ }
+
+ override func startLoading() {
+ MockURLProtocol.requestHistory.append(request)
+
+ guard let handler = MockURLProtocol.requestHandler else {
+ XCTFail("Received unexpected request with no handler set")
+ return
+ }
+ do {
+ let (response, data) = try handler(request)
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ client?.urlProtocol(self, didLoad: data)
+ client?.urlProtocolDidFinishLoading(self)
+ } catch {
+ client?.urlProtocol(self, didFailWithError: error)
+ }
+ }
+
+ override func stopLoading() {
+
+ }
+}
+
diff --git a/Orchestrate/README.md b/Orchestrate/README.md
new file mode 100644
index 0000000..3c38899
--- /dev/null
+++ b/Orchestrate/README.md
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+# PingOrchestrate
+
+## Overview
+
+PingOrchestrate provides a simple way to build a state machine for ForgeRock Journey and PingOne DaVinci.
+It allows you to define a series of states and transitions between them. You can use the workflow engine to build
+complex workflows that involve multiple steps and conditions.
+The Workflow engine allows you to define a series of functions and register them as a module to the `Workflow` instance
+in different state of the workflow.
+
+The Workflow engine contains the following states:
+
+
+
+| State | Description | Input | Output | Use Case |
+|------------|:--------------------------------------:|---------:|--------:|---------------------------------------------------------------------------:|
+| Init | Initialize the modules | () | Void | OAuth module loads endpoint from discovery URL |
+| Start | Start the flow | Request | Request | Intercept the start request, for example inject forceAuth parameter |
+| Response | Handle the response | Response | Void | Parse the response, for example store the cookie header. |
+| *Transform | Transform the response to Node | Response | Node | For Journey, transform response to Callback, for DaVinci transform to Form |
+| Node | Process the Node or Transform the Node | Node | Node | Transform MetadataCallback to WebAuthnCallback |
+| Next | Move to next state | Request | Request | Inject noSession parameter |
+| Success | Flow finished with Success | Success | Success | Prepare Success with AuthCode or Session |
+| SignOff | SignOff the Workflow | Request | Request | Revoke Token and end session |
+
+### Module
+
+Module allows you to register functions in different state of the workflow. For example, you can register a function
+that
+will be called when the workflow is initialized,
+when a node is received, when a node is sent, and when the workflow is started.
+
+
+
+Information can be shared across state, there are 2 contexts
+
+| Context | Scope | Access |
+|-----------------|:-----------------------------------:|-----------------------------------:|
+| WorkflowContext | Workflow Instance | ```context["name"] = "value" ``` |
+| FlowContext | Flow from Start to Finish (Success) | ```flowContext["name"]= "value"``` |
+
+## Integrating the SDK into your project
+
+Use Cocoapods or Swift Package Manger
+
+## Usage
+
+To use the `Workflow` class, you need to create an instance of it by passing a configuration block to the `createWorkflow` method. The
+configuration block allows you to register various modules of the `Workflow` instance.
+
+Here's an example of how to create a `Workflow` instance:
+
+```swift
+let workflow = Workflow.createWorkflow { config in
+ config.module(forceAuth)
+ config.module(noSession)
+ config.module(session)
+}
+_ = await workflow.start()
+```
+The `start` method returns a `Node` instance. The `Node` class represents the current state of the application. You can
+use the `next` method to transition to the next state.
+
+### SignOff
+There is a special state called `SignOff` that is used to sign off the user. You can use the `signOff` method to sign off
+the user.
+
+```swift
+ _ = await workflow.signOff()
+```
+
+## Custom Module
+
+You can provide a custom module to the `Workflow` instance. A module is a class that uses the `Module` interface.
+The `Module` interface allows the module to install `function`s in different states during the `Workflow` flow.
+
+```swift
+let customHeader = Module.of({ CustomHeaderConfig1() }, setup: { setup in
+ let config = setup.config
+ // Intercept all send request and inject custom header
+ setup.next { ( context, _, request) in
+ if config.enable {
+ request.header(name: config.headerName, value: config.headerValue)
+ }
+ return request
+ }
+})
+```
+
+You can then install the custom module in the `Workflow` configuration block like this:
+
+```swift
+let workflow = Workflow.createWorkflow { config in
+ config.module(customHeader) { header in
+ header.headerName = "iOS-SDK2"
+ header.headerValue = "headervalue3"
+ }
+}
+```
+
+More module examples:
+```swift
+let nosession = Module.of { setup in
+//Intercept all send request and inject custom header during start state
+ setup.next { ( context,_, request) in
+ request.header(name: "nosession", value: "true")
+ return request
+ }
+}
+
+
+let forceAuth = Module.of { setup in
+//Intercept all send request and inject custom header during start state
+ setup.start { ( context, request) in
+ request.header(name: "forceAuth", value: "true")
+ return request
+ }
+}
+```
diff --git a/Orchestrate/images/functions.png b/Orchestrate/images/functions.png
new file mode 100644
index 0000000..2e51fe4
Binary files /dev/null and b/Orchestrate/images/functions.png differ
diff --git a/Orchestrate/images/state.png b/Orchestrate/images/state.png
new file mode 100644
index 0000000..f0250a2
Binary files /dev/null and b/Orchestrate/images/state.png differ
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..74341bc
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,25 @@
+// swift-tools-version:5.3
+import PackageDescription
+
+let package = Package (
+ name: "Ping-SDK-iOS",
+ platforms: [
+ .iOS(.v13)
+ ],
+ products: [
+ .library(name: "PingLogger", targets: ["PingLogger"]),
+ .library(name: "PingStorage", targets: ["PingStorage"]),
+ .library(name: "PingOrchestrate", targets: ["PingOrchestrate"]),
+ .library(name: "PingOidc", targets: ["PingOidc"]),
+ .library(name: "PingDavinci", targets: ["PingDavinci"])
+ ],
+ dependencies: [
+ ],
+ targets: [
+ .target(name: "PingLogger", dependencies: [], path: "Logger/Logger", exclude: ["Logger.h"], resources: [.copy("PrivacyInfo.xcprivacy")]),
+ .target(name: "PingStorage", dependencies: [], path: "Storage/Storage", exclude: ["Storage.h"], resources: [.copy("PrivacyInfo.xcprivacy")]),
+ .target(name: "PingOrchestrate", dependencies: [.target(name: "PingLogger"), .target(name: "PingStorage")], path: "Orchestrate/Orchestrate", exclude: ["Orchestrate.h"], resources: [.copy("PrivacyInfo.xcprivacy")]),
+ .target(name: "PingOidc", dependencies: [.target(name: "PingOrchestrate")], path: "Oidc/Oidc", exclude: ["Oidc.h"], resources: [.copy("PrivacyInfo.xcprivacy")]),
+ .target(name: "PingDavinci", dependencies: [.target(name: "PingOidc"),], path: "Davinci/Davinci", exclude: ["Davinci.h"], resources: [.copy("PrivacyInfo.xcprivacy")]),
+ ]
+)
diff --git a/PingDavinci.podspec b/PingDavinci.podspec
new file mode 100644
index 0000000..ac88dee
--- /dev/null
+++ b/PingDavinci.podspec
@@ -0,0 +1,39 @@
+#
+# Be sure to run `pod lib lint PingDavinci.podspec' to ensure this is a
+# valid spec before submitting.
+#
+# Any lines starting with a # are optional, but their use is encouraged
+# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
+#
+
+Pod::Spec.new do |s|
+ s.name = 'PingDavinci'
+ s.version = '1.0.0'
+ s.summary = 'PingDavinci SDK for iOS'
+ s.description = <<-DESC
+ The PingDavinci SDK is a powerful and flexible library for Authentication and Authorization. It is designed to be easy to use and extensible. It provides a simple API for navigating the authentication flow and handling the various states that can
+occur during the authentication process.
+ DESC
+ s.homepage = 'https://www.pingidentity.com/'
+ s.license = { :type => 'MIT', :file => 'LICENSE' }
+ s.author = 'Ping Identity'
+
+ s.source = {
+ :git => 'https://github.com/ForgeRock/ping-ios-sdk.git',
+ :tag => s.version.to_s
+ }
+
+ s.module_name = 'PingDavinci'
+ s.swift_versions = ['5.0', '5.1']
+
+ s.ios.deployment_target = '13.0'
+
+ base_dir = "Davinci/Davinci"
+ s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.c', base_dir + '/**/*.h'
+ s.resource_bundles = {
+ 'Davinci' => [base_dir + '/*.xcprivacy']
+ }
+
+ s.ios.dependency 'PingOidc', '~> 1.0.0'
+
+end
diff --git a/PingLogger.podspec b/PingLogger.podspec
new file mode 100644
index 0000000..66bb921
--- /dev/null
+++ b/PingLogger.podspec
@@ -0,0 +1,35 @@
+#
+# Be sure to run `pod lib lint PingLogger.podspec' to ensure this is a
+# valid spec before submitting.
+#
+# Any lines starting with a # are optional, but their use is encouraged
+# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
+#
+
+Pod::Spec.new do |s|
+ s.name = 'PingLogger'
+ s.version = '1.0.0'
+ s.summary = 'PingLogger SDK for iOS'
+ s.description = <<-DESC
+ The PingLogger SDK provides a versatile logging interface and a set of common loggers for the Ping SDKs.
+ DESC
+ s.homepage = 'https://www.pingidentity.com/'
+ s.license = { :type => 'MIT', :file => 'LICENSE' }
+ s.author = 'Ping Identity'
+
+ s.source = {
+ :git => 'https://github.com/ForgeRock/ping-ios-sdk.git',
+ :tag => s.version.to_s
+ }
+
+ s.module_name = 'PingLogger'
+ s.swift_versions = ['5.0', '5.1']
+
+ s.ios.deployment_target = '13.0'
+
+ base_dir = "Logger/Logger"
+ s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.h'
+ s.resource_bundles = {
+ 'Logger' => [base_dir + '/*.xcprivacy']
+ }
+end
diff --git a/PingOidc.podspec b/PingOidc.podspec
new file mode 100644
index 0000000..02ee2b1
--- /dev/null
+++ b/PingOidc.podspec
@@ -0,0 +1,37 @@
+#
+# Be sure to run `pod lib lint PingOidc.podspec' to ensure this is a
+# valid spec before submitting.
+#
+# Any lines starting with a # are optional, but their use is encouraged
+# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
+#
+
+Pod::Spec.new do |s|
+ s.name = 'PingOidc'
+ s.version = '1.0.0'
+ s.summary = 'PingOidc SDK for iOS'
+ s.description = <<-DESC
+ The PingOidc SDK provides OIDC client for PingOne and ForgeRock platform.
+ DESC
+ s.homepage = 'https://www.pingidentity.com/'
+ s.license = { :type => 'MIT', :file => 'LICENSE' }
+ s.author = 'Ping Identity'
+
+ s.source = {
+ :git => 'https://github.com/ForgeRock/ping-ios-sdk.git',
+ :tag => s.version.to_s
+ }
+
+ s.module_name = 'PingOidc'
+ s.swift_versions = ['5.0', '5.1']
+
+ s.ios.deployment_target = '13.0'
+
+ base_dir = "Oidc/Oidc"
+ s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.h'
+ s.resource_bundles = {
+ 'Oidc' => [base_dir + '/*.xcprivacy']
+ }
+
+ s.ios.dependency 'PingOrchestrate', '~> 1.0.0'
+end
diff --git a/PingOrchestrate.podspec b/PingOrchestrate.podspec
new file mode 100644
index 0000000..afe2433
--- /dev/null
+++ b/PingOrchestrate.podspec
@@ -0,0 +1,38 @@
+#
+# Be sure to run `pod lib lint PingOrchestrate.podspec' to ensure this is a
+# valid spec before submitting.
+#
+# Any lines starting with a # are optional, but their use is encouraged
+# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
+#
+
+Pod::Spec.new do |s|
+ s.name = 'PingOrchestrate'
+ s.version = '1.0.0'
+ s.summary = 'PingOrchestrate SDK for iOS'
+ s.description = <<-DESC
+ The PingOrchestrate SDK provides a simple way to build a state machine for ForgeRock Journey and PingOne DaVinci.
+ DESC
+ s.homepage = 'https://www.pingidentity.com/'
+ s.license = { :type => 'MIT', :file => 'LICENSE' }
+ s.author = 'Ping Identity'
+
+ s.source = {
+ :git => 'https://github.com/ForgeRock/ping-ios-sdk.git',
+ :tag => s.version.to_s
+ }
+
+ s.module_name = 'PingOrchestrate'
+ s.swift_versions = ['5.0', '5.1']
+
+ s.ios.deployment_target = '13.0'
+
+ base_dir = "Orchestrate/Orchestrate"
+ s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.h'
+ s.resource_bundles = {
+ 'Orchestrate' => [base_dir + '/*.xcprivacy']
+ }
+
+ s.ios.dependency 'PingLogger', '~> 1.0.0'
+ s.ios.dependency 'PingStorage', '~> 1.0.0'
+end
diff --git a/PingStorage.podspec b/PingStorage.podspec
new file mode 100644
index 0000000..b4ecc5f
--- /dev/null
+++ b/PingStorage.podspec
@@ -0,0 +1,35 @@
+#
+# Be sure to run `pod lib lint PingStorage.podspec' to ensure this is a
+# valid spec before submitting.
+#
+# Any lines starting with a # are optional, but their use is encouraged
+# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
+#
+
+Pod::Spec.new do |s|
+ s.name = 'PingStorage'
+ s.version = '1.0.0'
+ s.summary = 'PingStorage SDK for iOS'
+ s.description = <<-DESC
+ The PingStorage SDK provides a flexible storage interface and a set of common storage solutions for the Ping SDKs.
+ DESC
+ s.homepage = 'https://www.pingidentity.com/'
+ s.license = { :type => 'MIT', :file => 'LICENSE' }
+ s.author = 'Ping Identity'
+
+ s.source = {
+ :git => 'https://github.com/ForgeRock/ping-ios-sdk.git',
+ :tag => s.version.to_s
+ }
+
+ s.module_name = 'PingStorage'
+ s.swift_versions = ['5.0', '5.1']
+
+ s.ios.deployment_target = '13.0'
+
+ base_dir = "Storage/Storage"
+ s.source_files = base_dir + '/**/*.swift', base_dir + '/**/*.c', base_dir + '/**/*.h'
+ s.resource_bundles = {
+ 'Storage' => [base_dir + '/*.xcprivacy']
+ }
+end
diff --git a/PingTestHost/PingTestHost.xcodeproj/project.pbxproj b/PingTestHost/PingTestHost.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..3b37e25
--- /dev/null
+++ b/PingTestHost/PingTestHost.xcodeproj/project.pbxproj
@@ -0,0 +1,450 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ A507A2672CEBED9F007F5C16 /* PingDavinci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2622CEBED9F007F5C16 /* PingDavinci.framework */; };
+ A507A2682CEBED9F007F5C16 /* PingDavinci.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2622CEBED9F007F5C16 /* PingDavinci.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A507A2692CEBED9F007F5C16 /* PingLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2632CEBED9F007F5C16 /* PingLogger.framework */; };
+ A507A26A2CEBED9F007F5C16 /* PingLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2632CEBED9F007F5C16 /* PingLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A507A26B2CEBED9F007F5C16 /* PingOidc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2642CEBED9F007F5C16 /* PingOidc.framework */; };
+ A507A26C2CEBED9F007F5C16 /* PingOidc.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2642CEBED9F007F5C16 /* PingOidc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A507A26D2CEBED9F007F5C16 /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2652CEBED9F007F5C16 /* PingOrchestrate.framework */; };
+ A507A26E2CEBED9F007F5C16 /* PingOrchestrate.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2652CEBED9F007F5C16 /* PingOrchestrate.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A507A26F2CEBED9F007F5C16 /* PingStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2662CEBED9F007F5C16 /* PingStorage.framework */; };
+ A507A2702CEBED9F007F5C16 /* PingStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A507A2662CEBED9F007F5C16 /* PingStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A54BF4692BF2E33C00CD24D4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF4682BF2E33C00CD24D4 /* AppDelegate.swift */; };
+ A54BF46B2BF2E33C00CD24D4 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF46A2BF2E33C00CD24D4 /* SceneDelegate.swift */; };
+ A54BF46D2BF2E33C00CD24D4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF46C2BF2E33C00CD24D4 /* ViewController.swift */; };
+ A54BF4702BF2E33C00CD24D4 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = A54BF46F2BF2E33C00CD24D4 /* Base */; };
+ A54BF4722BF2E34000CD24D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A54BF4712BF2E34000CD24D4 /* Assets.xcassets */; };
+ A54BF4752BF2E34000CD24D4 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = A54BF4742BF2E34000CD24D4 /* Base */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ A507A2712CEBED9F007F5C16 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ A507A26A2CEBED9F007F5C16 /* PingLogger.framework in Embed Frameworks */,
+ A507A26C2CEBED9F007F5C16 /* PingOidc.framework in Embed Frameworks */,
+ A507A2682CEBED9F007F5C16 /* PingDavinci.framework in Embed Frameworks */,
+ A507A2702CEBED9F007F5C16 /* PingStorage.framework in Embed Frameworks */,
+ A507A26E2CEBED9F007F5C16 /* PingOrchestrate.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ A507A2622CEBED9F007F5C16 /* PingDavinci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingDavinci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A507A2632CEBED9F007F5C16 /* PingLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A507A2642CEBED9F007F5C16 /* PingOidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOidc.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A507A2652CEBED9F007F5C16 /* PingOrchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOrchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A507A2662CEBED9F007F5C16 /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A50966F02C50522300A4E5B5 /* PingOidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOidc.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A51D4CE62C656D7D00FE09E0 /* PingOrchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingOrchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A51D4CE82C656D7D00FE09E0 /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A51D4CEA2C656D8E00FE09E0 /* PingDavinci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingDavinci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A54BF4652BF2E33C00CD24D4 /* PingTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PingTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ A54BF4682BF2E33C00CD24D4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ A54BF46A2BF2E33C00CD24D4 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ A54BF46C2BF2E33C00CD24D4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
+ A54BF46F2BF2E33C00CD24D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ A54BF4712BF2E34000CD24D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ A54BF4742BF2E34000CD24D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ A54BF4762BF2E34000CD24D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ A54BF4832BF2E5B800CD24D4 /* PingLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A57A08972CE40FDE006F0CBB /* Storage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Storage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A57A089D2CE412E8006F0CBB /* Logger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Logger.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A57A08BB2CE4FD96006F0CBB /* Orchestrate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Orchestrate.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A57A08C22CE50537006F0CBB /* Oidc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Oidc.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A57A08DE2CE50937006F0CBB /* Davinci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Davinci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A5D1CD5E2BF504E400D1C83E /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ A54BF4622BF2E33C00CD24D4 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A507A2692CEBED9F007F5C16 /* PingLogger.framework in Frameworks */,
+ A507A26B2CEBED9F007F5C16 /* PingOidc.framework in Frameworks */,
+ A507A2672CEBED9F007F5C16 /* PingDavinci.framework in Frameworks */,
+ A507A26F2CEBED9F007F5C16 /* PingStorage.framework in Frameworks */,
+ A507A26D2CEBED9F007F5C16 /* PingOrchestrate.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ A54BF45C2BF2E33C00CD24D4 = {
+ isa = PBXGroup;
+ children = (
+ A54BF4672BF2E33C00CD24D4 /* PingTestHost */,
+ A54BF4662BF2E33C00CD24D4 /* Products */,
+ A54BF4822BF2E5B800CD24D4 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ A54BF4662BF2E33C00CD24D4 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A54BF4652BF2E33C00CD24D4 /* PingTestHost.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ A54BF4672BF2E33C00CD24D4 /* PingTestHost */ = {
+ isa = PBXGroup;
+ children = (
+ A54BF4682BF2E33C00CD24D4 /* AppDelegate.swift */,
+ A54BF46A2BF2E33C00CD24D4 /* SceneDelegate.swift */,
+ A54BF46C2BF2E33C00CD24D4 /* ViewController.swift */,
+ A54BF46E2BF2E33C00CD24D4 /* Main.storyboard */,
+ A54BF4712BF2E34000CD24D4 /* Assets.xcassets */,
+ A54BF4732BF2E34000CD24D4 /* LaunchScreen.storyboard */,
+ A54BF4762BF2E34000CD24D4 /* Info.plist */,
+ );
+ path = PingTestHost;
+ sourceTree = "";
+ };
+ A54BF4822BF2E5B800CD24D4 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ A507A2622CEBED9F007F5C16 /* PingDavinci.framework */,
+ A507A2632CEBED9F007F5C16 /* PingLogger.framework */,
+ A507A2642CEBED9F007F5C16 /* PingOidc.framework */,
+ A507A2652CEBED9F007F5C16 /* PingOrchestrate.framework */,
+ A507A2662CEBED9F007F5C16 /* PingStorage.framework */,
+ A57A08DE2CE50937006F0CBB /* Davinci.framework */,
+ A57A08C22CE50537006F0CBB /* Oidc.framework */,
+ A57A08BB2CE4FD96006F0CBB /* Orchestrate.framework */,
+ A57A089D2CE412E8006F0CBB /* Logger.framework */,
+ A57A08972CE40FDE006F0CBB /* Storage.framework */,
+ A51D4CEA2C656D8E00FE09E0 /* PingDavinci.framework */,
+ A51D4CE62C656D7D00FE09E0 /* PingOrchestrate.framework */,
+ A51D4CE82C656D7D00FE09E0 /* PingStorage.framework */,
+ A50966F02C50522300A4E5B5 /* PingOidc.framework */,
+ A5D1CD5E2BF504E400D1C83E /* PingStorage.framework */,
+ A54BF4832BF2E5B800CD24D4 /* PingLogger.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ A54BF4642BF2E33C00CD24D4 /* PingTestHost */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = A54BF4792BF2E34000CD24D4 /* Build configuration list for PBXNativeTarget "PingTestHost" */;
+ buildPhases = (
+ A54BF4612BF2E33C00CD24D4 /* Sources */,
+ A54BF4622BF2E33C00CD24D4 /* Frameworks */,
+ A54BF4632BF2E33C00CD24D4 /* Resources */,
+ A507A2712CEBED9F007F5C16 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PingTestHost;
+ productName = PingTestHost;
+ productReference = A54BF4652BF2E33C00CD24D4 /* PingTestHost.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ A54BF45D2BF2E33C00CD24D4 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1530;
+ LastUpgradeCheck = 1530;
+ TargetAttributes = {
+ A54BF4642BF2E33C00CD24D4 = {
+ CreatedOnToolsVersion = 15.3;
+ };
+ };
+ };
+ buildConfigurationList = A54BF4602BF2E33C00CD24D4 /* Build configuration list for PBXProject "PingTestHost" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = A54BF45C2BF2E33C00CD24D4;
+ productRefGroup = A54BF4662BF2E33C00CD24D4 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ A54BF4642BF2E33C00CD24D4 /* PingTestHost */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ A54BF4632BF2E33C00CD24D4 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A54BF4722BF2E34000CD24D4 /* Assets.xcassets in Resources */,
+ A54BF4752BF2E34000CD24D4 /* Base in Resources */,
+ A54BF4702BF2E33C00CD24D4 /* Base in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ A54BF4612BF2E33C00CD24D4 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A54BF46D2BF2E33C00CD24D4 /* ViewController.swift in Sources */,
+ A54BF4692BF2E33C00CD24D4 /* AppDelegate.swift in Sources */,
+ A54BF46B2BF2E33C00CD24D4 /* SceneDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ A54BF46E2BF2E33C00CD24D4 /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ A54BF46F2BF2E33C00CD24D4 /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ A54BF4732BF2E34000CD24D4 /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ A54BF4742BF2E34000CD24D4 /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ A54BF4772BF2E34000CD24D4 /* 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 = 13.0;
+ 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";
+ };
+ name = Debug;
+ };
+ A54BF4782BF2E34000CD24D4 /* 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 = 13.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ A54BF47A2BF2E34000CD24D4 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = PingTestHost/Info.plist;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
+ INFOPLIST_KEY_UIMainStoryboardFile = Main;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingTestHost;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Debug;
+ };
+ A54BF47B2BF2E34000CD24D4 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = PingTestHost/Info.plist;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
+ INFOPLIST_KEY_UIMainStoryboardFile = Main;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingTestHost;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ A54BF4602BF2E33C00CD24D4 /* Build configuration list for PBXProject "PingTestHost" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A54BF4772BF2E34000CD24D4 /* Debug */,
+ A54BF4782BF2E34000CD24D4 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ A54BF4792BF2E34000CD24D4 /* Build configuration list for PBXNativeTarget "PingTestHost" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A54BF47A2BF2E34000CD24D4 /* Debug */,
+ A54BF47B2BF2E34000CD24D4 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = A54BF45D2BF2E33C00CD24D4 /* Project object */;
+}
diff --git a/PingTestHost/PingTestHost.xcodeproj/xcshareddata/IDETemplateMacros.plist b/PingTestHost/PingTestHost.xcodeproj/xcshareddata/IDETemplateMacros.plist
new file mode 100644
index 0000000..16cf018
--- /dev/null
+++ b/PingTestHost/PingTestHost.xcodeproj/xcshareddata/IDETemplateMacros.plist
@@ -0,0 +1,17 @@
+
+
+
+
+ FILEHEADER
+
+// ___FILENAME___
+// ___PACKAGENAME___
+//
+// Copyright (c) ___YEAR___ Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+
diff --git a/PingTestHost/PingTestHost.xcodeproj/xcshareddata/xcschemes/PingTestHost.xcscheme b/PingTestHost/PingTestHost.xcodeproj/xcshareddata/xcschemes/PingTestHost.xcscheme
new file mode 100644
index 0000000..90ba3ae
--- /dev/null
+++ b/PingTestHost/PingTestHost.xcodeproj/xcshareddata/xcschemes/PingTestHost.xcscheme
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PingTestHost/PingTestHost.xctestplan b/PingTestHost/PingTestHost.xctestplan
new file mode 100644
index 0000000..3cf159d
--- /dev/null
+++ b/PingTestHost/PingTestHost.xctestplan
@@ -0,0 +1,60 @@
+{
+ "configurations" : [
+ {
+ "id" : "653B9364-499B-4DFE-A77C-201EEBA63A50",
+ "name" : "Test Scheme Action",
+ "options" : {
+
+ }
+ }
+ ],
+ "defaultOptions" : {
+ "targetForVariableExpansion" : {
+ "containerPath" : "container:PingTestHost.xcodeproj",
+ "identifier" : "A54BF4642BF2E33C00CD24D4",
+ "name" : "PingTestHost"
+ }
+ },
+ "testTargets" : [
+ {
+ "target" : {
+ "containerPath" : "container:..\/Logger\/Logger.xcodeproj",
+ "identifier" : "A5A796B52BE04E68004D0F2D",
+ "name" : "LoggerTests"
+ }
+ },
+ {
+ "target" : {
+ "containerPath" : "container:..\/Storage\/Storage.xcodeproj",
+ "identifier" : "A5A797032BE1782F004D0F2D",
+ "name" : "StorageTests"
+ }
+ },
+ {
+ "target" : {
+ "containerPath" : "container:..\/Oidc\/Oidc.xcodeproj",
+ "identifier" : "3AB1CA0E2BD6F99C003FCE3C",
+ "name" : "OidcTests"
+ }
+ },
+ {
+ "target" : {
+ "containerPath" : "container:..\/Orchestrate\/Orchestrate.xcodeproj",
+ "identifier" : "3A54417D2BCDF1D900385131",
+ "name" : "OrchestrateTests"
+ }
+ },
+ {
+ "skippedTests" : [
+ "DaVinciIntegrationTests\/testHappyPathWithPasswordCredentials()",
+ "DaVinciIntegrationTests\/testInvalidPassword()"
+ ],
+ "target" : {
+ "containerPath" : "container:..\/Davinci\/Davinci.xcodeproj",
+ "identifier" : "3A5441A82BCDF20700385131",
+ "name" : "DavinciTests"
+ }
+ }
+ ],
+ "version" : 1
+}
diff --git a/PingTestHost/PingTestHost/AppDelegate.swift b/PingTestHost/PingTestHost/AppDelegate.swift
new file mode 100644
index 0000000..e6a6f85
--- /dev/null
+++ b/PingTestHost/PingTestHost/AppDelegate.swift
@@ -0,0 +1,39 @@
+//
+// AppDelegate.swift
+// PingTestHost
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+import UIKit
+
+@main
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+
+
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ // Override point for customization after application launch.
+ return true
+ }
+
+ // MARK: UISceneSession Lifecycle
+
+ func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
+ // Called when a new scene session is being created.
+ // Use this method to select a configuration to create the new scene with.
+ return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
+ }
+
+ func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
+ // Called when the user discards a scene session.
+ // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
+ // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
+ }
+
+
+}
+
diff --git a/PingTestHost/PingTestHost/Assets.xcassets/AccentColor.colorset/Contents.json b/PingTestHost/PingTestHost/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/PingTestHost/PingTestHost/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/PingTestHost/PingTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json b/PingTestHost/PingTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..13613e3
--- /dev/null
+++ b/PingTestHost/PingTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/PingTestHost/PingTestHost/Assets.xcassets/Contents.json b/PingTestHost/PingTestHost/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/PingTestHost/PingTestHost/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/PingTestHost/PingTestHost/Base.lproj/LaunchScreen.storyboard b/PingTestHost/PingTestHost/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..865e932
--- /dev/null
+++ b/PingTestHost/PingTestHost/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PingTestHost/PingTestHost/Base.lproj/Main.storyboard b/PingTestHost/PingTestHost/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..25a7638
--- /dev/null
+++ b/PingTestHost/PingTestHost/Base.lproj/Main.storyboard
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PingTestHost/PingTestHost/Info.plist b/PingTestHost/PingTestHost/Info.plist
new file mode 100644
index 0000000..dd3c9af
--- /dev/null
+++ b/PingTestHost/PingTestHost/Info.plist
@@ -0,0 +1,25 @@
+
+
+
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+
+
+
diff --git a/PingTestHost/PingTestHost/SceneDelegate.swift b/PingTestHost/PingTestHost/SceneDelegate.swift
new file mode 100644
index 0000000..2dccc5a
--- /dev/null
+++ b/PingTestHost/PingTestHost/SceneDelegate.swift
@@ -0,0 +1,55 @@
+//
+// SceneDelegate.swift
+// PingTestHost
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+import UIKit
+
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+
+ var window: UIWindow?
+
+
+ func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
+ // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
+ // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
+ // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
+ guard let _ = (scene as? UIWindowScene) else { return }
+ }
+
+ func sceneDidDisconnect(_ scene: UIScene) {
+ // Called as the scene is being released by the system.
+ // This occurs shortly after the scene enters the background, or when its session is discarded.
+ // Release any resources associated with this scene that can be re-created the next time the scene connects.
+ // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
+ }
+
+ func sceneDidBecomeActive(_ scene: UIScene) {
+ // Called when the scene has moved from an inactive state to an active state.
+ // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
+ }
+
+ func sceneWillResignActive(_ scene: UIScene) {
+ // Called when the scene will move from an active state to an inactive state.
+ // This may occur due to temporary interruptions (ex. an incoming phone call).
+ }
+
+ func sceneWillEnterForeground(_ scene: UIScene) {
+ // Called as the scene transitions from the background to the foreground.
+ // Use this method to undo the changes made on entering the background.
+ }
+
+ func sceneDidEnterBackground(_ scene: UIScene) {
+ // Called as the scene transitions from the foreground to the background.
+ // Use this method to save data, release shared resources, and store enough scene-specific state information
+ // to restore the scene back to its current state.
+ }
+
+
+}
+
diff --git a/PingTestHost/PingTestHost/ViewController.swift b/PingTestHost/PingTestHost/ViewController.swift
new file mode 100644
index 0000000..cf921f1
--- /dev/null
+++ b/PingTestHost/PingTestHost/ViewController.swift
@@ -0,0 +1,22 @@
+//
+// ViewController.swift
+// PingTestHost
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+import UIKit
+
+class ViewController: UIViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ // Do any additional setup after loading the view.
+ }
+
+
+}
+
diff --git a/README.md b/README.md
index 65a1659..c655664 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,19 @@
-# unified-sdk-ios
-Initial Repo for the Unified SDK - iOS
+[![Build](https://github.com/ForgeRock/unified-sdk-ios/actions/workflows/ci.yaml/badge.svg)](https://github.com/ForgeRock/unified-sdk-ios/actions/workflows/ci.yaml)
+
+
+
+
+
+
+
+
+The Ping SDK for iOS is designed for creating mobile native apps that seamlessly integrate with the PingOne platform.
+It offers a range of APIs for user authentication, user device management, and accessing resources secured by PingOne.
+
+
+
+
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
diff --git a/SampleApps/Ping.xcworkspace/contents.xcworkspacedata b/SampleApps/Ping.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..ebf3bea
--- /dev/null
+++ b/SampleApps/Ping.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/SampleApps/Ping.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SampleApps/Ping.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/SampleApps/Ping.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj b/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..a0dba91
--- /dev/null
+++ b/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj
@@ -0,0 +1,893 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 63;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 3A3709962BEB038200AA7B51 /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3709952BEB038200AA7B51 /* AccessToken.swift */; };
+ 3A5441332BCDF10900385131 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441322BCDF10900385131 /* ContentView.swift */; };
+ 3A5441352BCDF10B00385131 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A5441342BCDF10B00385131 /* Assets.xcassets */; };
+ 3A5441382BCDF10B00385131 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A5441372BCDF10B00385131 /* Preview Assets.xcassets */; };
+ 3A5441422BCDF10B00385131 /* PingExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441412BCDF10B00385131 /* PingExampleTests.swift */; };
+ 3A54414C2BCDF10B00385131 /* PingExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A54414B2BCDF10B00385131 /* PingExampleUITests.swift */; };
+ 3A54414E2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A54414D2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift */; };
+ 3A8092F42BE99F0F00AD667F /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8092F32BE99F0F00AD667F /* InputView.swift */; };
+ 3A8092F62BE9BB6B00AD667F /* TokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8092F52BE9BB6B00AD667F /* TokenViewModel.swift */; };
+ 3A8532332BE2D5D800F8619D /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8532322BE2D5D800F8619D /* LoginView.swift */; };
+ 3AB1C9F42BD6C6F3003FCE3C /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1C9F32BD6C6F3003FCE3C /* LoginViewModel.swift */; };
+ 3ACD90E12BE586BC00DABCE6 /* DavinciViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACD90E02BE586BC00DABCE6 /* DavinciViewModel.swift */; };
+ 3AE945742BF27224007B3381 /* LoginUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE945732BF27223007B3381 /* LoginUtility.swift */; };
+ A5560CA02CFE4EDA0076CA8D /* PingDavinci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A091A2CE511E7006F0CBB /* PingDavinci.framework */; };
+ A5560CA12CFE4EDA0076CA8D /* PingDavinci.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A091A2CE511E7006F0CBB /* PingDavinci.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A5560CA22CFE4EDA0076CA8D /* PingLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A08FA2CE50D51006F0CBB /* PingLogger.framework */; };
+ A5560CA32CFE4EDA0076CA8D /* PingLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A08FA2CE50D51006F0CBB /* PingLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A5560CA42CFE4EDA0076CA8D /* PingOidc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A09122CE51162006F0CBB /* PingOidc.framework */; };
+ A5560CA52CFE4EDA0076CA8D /* PingOidc.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A09122CE51162006F0CBB /* PingOidc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A5560CA62CFE4EDA0076CA8D /* PingOrchestrate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A090A2CE50FA5006F0CBB /* PingOrchestrate.framework */; };
+ A5560CA72CFE4EDA0076CA8D /* PingOrchestrate.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A090A2CE50FA5006F0CBB /* PingOrchestrate.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A5560CA82CFE4EDA0076CA8D /* PingStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A57A09022CE50EF1006F0CBB /* PingStorage.framework */; };
+ A5560CA92CFE4EDA0076CA8D /* PingStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A57A09022CE50EF1006F0CBB /* PingStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A588D0062BF6A0E500F9052A /* StorageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A588D0042BF6A0E500F9052A /* StorageViewModel.swift */; };
+ A588D0072BF6A0E500F9052A /* LoggerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A588D0052BF6A0E500F9052A /* LoggerViewModel.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 3A54413E2BCDF10B00385131 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3A5441252BCDF10900385131 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 3A54412C2BCDF10900385131;
+ remoteInfo = PingExample;
+ };
+ 3A5441482BCDF10B00385131 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3A5441252BCDF10900385131 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 3A54412C2BCDF10900385131;
+ remoteInfo = PingExample;
+ };
+ A57A08F92CE50D51006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = A5A796AE2BE04E67004D0F2D;
+ remoteInfo = Logger;
+ };
+ A57A08FB2CE50D51006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = A5A796B62BE04E68004D0F2D;
+ remoteInfo = LoggerTests;
+ };
+ A57A09012CE50EF1006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = A588CFF92BF69F3300F9052A /* Storage.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = A5A796FA2BE1782E004D0F2D;
+ remoteInfo = Storage;
+ };
+ A57A09032CE50EF1006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = A588CFF92BF69F3300F9052A /* Storage.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = A5A797042BE1782F004D0F2D;
+ remoteInfo = StorageTests;
+ };
+ A57A09092CE50FA5006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 3A5441742BCDF1D900385131;
+ remoteInfo = Orchestrate;
+ };
+ A57A090B2CE50FA5006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 3A54417E2BCDF1D900385131;
+ remoteInfo = OrchestrateTests;
+ };
+ A57A09112CE51162006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 3AB1CA072BD6F99C003FCE3C;
+ remoteInfo = Oidc;
+ };
+ A57A09132CE51162006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 3AB1CA0F2BD6F99C003FCE3C;
+ remoteInfo = OidcTests;
+ };
+ A57A09192CE511E7006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 3A5441A12BCDF20700385131;
+ remoteInfo = Davinci;
+ };
+ A57A091B2CE511E7006F0CBB /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 3A5441A92BCDF20700385131;
+ remoteInfo = DavinciTests;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 3A203D872BDB1A900020C995 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ A5560CA32CFE4EDA0076CA8D /* PingLogger.framework in Embed Frameworks */,
+ A5560CA52CFE4EDA0076CA8D /* PingOidc.framework in Embed Frameworks */,
+ A5560CA12CFE4EDA0076CA8D /* PingDavinci.framework in Embed Frameworks */,
+ A5560CA92CFE4EDA0076CA8D /* PingStorage.framework in Embed Frameworks */,
+ A5560CA72CFE4EDA0076CA8D /* PingOrchestrate.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 3A3709952BEB038200AA7B51 /* AccessToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = ""; };
+ 3A54412D2BCDF10900385131 /* PingExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PingExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3A5441322BCDF10900385131 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 3A5441342BCDF10B00385131 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 3A5441372BCDF10B00385131 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 3A54413D2BCDF10B00385131 /* PingExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PingExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3A5441412BCDF10B00385131 /* PingExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingExampleTests.swift; sourceTree = ""; };
+ 3A5441472BCDF10B00385131 /* PingExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PingExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3A54414B2BCDF10B00385131 /* PingExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingExampleUITests.swift; sourceTree = ""; };
+ 3A54414D2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingExampleUITestsLaunchTests.swift; sourceTree = ""; };
+ 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Davinci.xcodeproj; path = ../../Davinci/Davinci.xcodeproj; sourceTree = ""; };
+ 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Orchestrate.xcodeproj; path = ../../Orchestrate/Orchestrate.xcodeproj; sourceTree = ""; };
+ 3A8092F32BE99F0F00AD667F /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = ""; };
+ 3A8092F52BE9BB6B00AD667F /* TokenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenViewModel.swift; sourceTree = ""; };
+ 3A8532322BE2D5D800F8619D /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; };
+ 3AB1C9F32BD6C6F3003FCE3C /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; };
+ 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Oidc.xcodeproj; path = ../../Oidc/Oidc.xcodeproj; sourceTree = ""; };
+ 3ACD90E02BE586BC00DABCE6 /* DavinciViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DavinciViewModel.swift; sourceTree = ""; };
+ 3AE945732BF27223007B3381 /* LoginUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginUtility.swift; sourceTree = ""; };
+ A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Logger.xcodeproj; path = ../../Logger/Logger.xcodeproj; sourceTree = ""; };
+ A588CFF92BF69F3300F9052A /* Storage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Storage.xcodeproj; path = ../../Storage/Storage.xcodeproj; sourceTree = ""; };
+ A588D0042BF6A0E500F9052A /* StorageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageViewModel.swift; sourceTree = ""; };
+ A588D0052BF6A0E500F9052A /* LoggerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggerViewModel.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 3A54412A2BCDF10900385131 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5560CA22CFE4EDA0076CA8D /* PingLogger.framework in Frameworks */,
+ A5560CA42CFE4EDA0076CA8D /* PingOidc.framework in Frameworks */,
+ A5560CA02CFE4EDA0076CA8D /* PingDavinci.framework in Frameworks */,
+ A5560CA82CFE4EDA0076CA8D /* PingStorage.framework in Frameworks */,
+ A5560CA62CFE4EDA0076CA8D /* PingOrchestrate.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A54413A2BCDF10B00385131 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A5441442BCDF10B00385131 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 3A5441242BCDF10900385131 = {
+ isa = PBXGroup;
+ children = (
+ A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */,
+ A588CFF92BF69F3300F9052A /* Storage.xcodeproj */,
+ 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */,
+ 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */,
+ 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */,
+ 3A54412F2BCDF10900385131 /* PingExample */,
+ 3A5441402BCDF10B00385131 /* PingExampleTests */,
+ 3A54414A2BCDF10B00385131 /* PingExampleUITests */,
+ 3A54412E2BCDF10900385131 /* Products */,
+ 3A5441642BCDF17600385131 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 3A54412E2BCDF10900385131 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 3A54412D2BCDF10900385131 /* PingExample.app */,
+ 3A54413D2BCDF10B00385131 /* PingExampleTests.xctest */,
+ 3A5441472BCDF10B00385131 /* PingExampleUITests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 3A54412F2BCDF10900385131 /* PingExample */ = {
+ isa = PBXGroup;
+ children = (
+ 3AE945732BF27223007B3381 /* LoginUtility.swift */,
+ 3A5441322BCDF10900385131 /* ContentView.swift */,
+ 3A8532322BE2D5D800F8619D /* LoginView.swift */,
+ 3A3709952BEB038200AA7B51 /* AccessToken.swift */,
+ 3AB1C9F32BD6C6F3003FCE3C /* LoginViewModel.swift */,
+ 3ACD90E02BE586BC00DABCE6 /* DavinciViewModel.swift */,
+ 3A8092F52BE9BB6B00AD667F /* TokenViewModel.swift */,
+ 3A8092F32BE99F0F00AD667F /* InputView.swift */,
+ A588D0052BF6A0E500F9052A /* LoggerViewModel.swift */,
+ A588D0042BF6A0E500F9052A /* StorageViewModel.swift */,
+ 3A5441342BCDF10B00385131 /* Assets.xcassets */,
+ 3A5441362BCDF10B00385131 /* Preview Content */,
+ );
+ path = PingExample;
+ sourceTree = "";
+ };
+ 3A5441362BCDF10B00385131 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 3A5441372BCDF10B00385131 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ 3A5441402BCDF10B00385131 /* PingExampleTests */ = {
+ isa = PBXGroup;
+ children = (
+ 3A5441412BCDF10B00385131 /* PingExampleTests.swift */,
+ );
+ path = PingExampleTests;
+ sourceTree = "";
+ };
+ 3A54414A2BCDF10B00385131 /* PingExampleUITests */ = {
+ isa = PBXGroup;
+ children = (
+ 3A54414B2BCDF10B00385131 /* PingExampleUITests.swift */,
+ 3A54414D2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift */,
+ );
+ path = PingExampleUITests;
+ sourceTree = "";
+ };
+ 3A5441642BCDF17600385131 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ A57A08F52CE50D51006F0CBB /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A57A08FA2CE50D51006F0CBB /* PingLogger.framework */,
+ A57A08FC2CE50D51006F0CBB /* LoggerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ A57A08FD2CE50EF1006F0CBB /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A57A09022CE50EF1006F0CBB /* PingStorage.framework */,
+ A57A09042CE50EF1006F0CBB /* StorageTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ A57A09052CE50FA5006F0CBB /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A57A090A2CE50FA5006F0CBB /* PingOrchestrate.framework */,
+ A57A090C2CE50FA5006F0CBB /* OrchestrateTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ A57A090D2CE51162006F0CBB /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A57A09122CE51162006F0CBB /* PingOidc.framework */,
+ A57A09142CE51162006F0CBB /* OidcTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ A57A09152CE511E7006F0CBB /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A57A091A2CE511E7006F0CBB /* PingDavinci.framework */,
+ A57A091C2CE511E7006F0CBB /* DavinciTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 3A54412C2BCDF10900385131 /* PingExample */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3A5441512BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExample" */;
+ buildPhases = (
+ 3A5441292BCDF10900385131 /* Sources */,
+ 3A54412A2BCDF10900385131 /* Frameworks */,
+ 3A54412B2BCDF10900385131 /* Resources */,
+ 3A203D872BDB1A900020C995 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PingExample;
+ productName = PingExample;
+ productReference = 3A54412D2BCDF10900385131 /* PingExample.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 3A54413C2BCDF10B00385131 /* PingExampleTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3A5441542BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExampleTests" */;
+ buildPhases = (
+ 3A5441392BCDF10B00385131 /* Sources */,
+ 3A54413A2BCDF10B00385131 /* Frameworks */,
+ 3A54413B2BCDF10B00385131 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 3A54413F2BCDF10B00385131 /* PBXTargetDependency */,
+ );
+ name = PingExampleTests;
+ productName = PingExampleTests;
+ productReference = 3A54413D2BCDF10B00385131 /* PingExampleTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 3A5441462BCDF10B00385131 /* PingExampleUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3A5441572BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExampleUITests" */;
+ buildPhases = (
+ 3A5441432BCDF10B00385131 /* Sources */,
+ 3A5441442BCDF10B00385131 /* Frameworks */,
+ 3A5441452BCDF10B00385131 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 3A5441492BCDF10B00385131 /* PBXTargetDependency */,
+ );
+ name = PingExampleUITests;
+ productName = PingExampleUITests;
+ productReference = 3A5441472BCDF10B00385131 /* PingExampleUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 3A5441252BCDF10900385131 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1500;
+ TargetAttributes = {
+ 3A54412C2BCDF10900385131 = {
+ CreatedOnToolsVersion = 15.0;
+ };
+ 3A54413C2BCDF10B00385131 = {
+ CreatedOnToolsVersion = 15.0;
+ TestTargetID = 3A54412C2BCDF10900385131;
+ };
+ 3A5441462BCDF10B00385131 = {
+ CreatedOnToolsVersion = 15.0;
+ TestTargetID = 3A54412C2BCDF10900385131;
+ };
+ };
+ };
+ buildConfigurationList = 3A5441282BCDF10900385131 /* Build configuration list for PBXProject "PingExample" */;
+ compatibilityVersion = "Xcode 15.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 3A5441242BCDF10900385131;
+ productRefGroup = 3A54412E2BCDF10900385131 /* Products */;
+ projectDirPath = "";
+ projectReferences = (
+ {
+ ProductGroup = A57A09152CE511E7006F0CBB /* Products */;
+ ProjectRef = 3A54420F2BCE1F2900385131 /* Davinci.xcodeproj */;
+ },
+ {
+ ProductGroup = A57A08F52CE50D51006F0CBB /* Products */;
+ ProjectRef = A588CFEA2BF69F1A00F9052A /* Logger.xcodeproj */;
+ },
+ {
+ ProductGroup = A57A090D2CE51162006F0CBB /* Products */;
+ ProjectRef = 3AB1CA1F2BD6F9AD003FCE3C /* Oidc.xcodeproj */;
+ },
+ {
+ ProductGroup = A57A09052CE50FA5006F0CBB /* Products */;
+ ProjectRef = 3A5442212BCE1F3800385131 /* Orchestrate.xcodeproj */;
+ },
+ {
+ ProductGroup = A57A08FD2CE50EF1006F0CBB /* Products */;
+ ProjectRef = A588CFF92BF69F3300F9052A /* Storage.xcodeproj */;
+ },
+ );
+ projectRoot = "";
+ targets = (
+ 3A54412C2BCDF10900385131 /* PingExample */,
+ 3A54413C2BCDF10B00385131 /* PingExampleTests */,
+ 3A5441462BCDF10B00385131 /* PingExampleUITests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXReferenceProxy section */
+ A57A08FA2CE50D51006F0CBB /* PingLogger.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = PingLogger.framework;
+ remoteRef = A57A08F92CE50D51006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ A57A08FC2CE50D51006F0CBB /* LoggerTests.xctest */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.cfbundle;
+ path = LoggerTests.xctest;
+ remoteRef = A57A08FB2CE50D51006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ A57A09022CE50EF1006F0CBB /* PingStorage.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = PingStorage.framework;
+ remoteRef = A57A09012CE50EF1006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ A57A09042CE50EF1006F0CBB /* StorageTests.xctest */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.cfbundle;
+ path = StorageTests.xctest;
+ remoteRef = A57A09032CE50EF1006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ A57A090A2CE50FA5006F0CBB /* PingOrchestrate.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = PingOrchestrate.framework;
+ remoteRef = A57A09092CE50FA5006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ A57A090C2CE50FA5006F0CBB /* OrchestrateTests.xctest */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.cfbundle;
+ path = OrchestrateTests.xctest;
+ remoteRef = A57A090B2CE50FA5006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ A57A09122CE51162006F0CBB /* PingOidc.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = PingOidc.framework;
+ remoteRef = A57A09112CE51162006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ A57A09142CE51162006F0CBB /* OidcTests.xctest */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.cfbundle;
+ path = OidcTests.xctest;
+ remoteRef = A57A09132CE51162006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ A57A091A2CE511E7006F0CBB /* PingDavinci.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = PingDavinci.framework;
+ remoteRef = A57A09192CE511E7006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ A57A091C2CE511E7006F0CBB /* DavinciTests.xctest */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.cfbundle;
+ path = DavinciTests.xctest;
+ remoteRef = A57A091B2CE511E7006F0CBB /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+/* End PBXReferenceProxy section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 3A54412B2BCDF10900385131 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3A5441382BCDF10B00385131 /* Preview Assets.xcassets in Resources */,
+ 3A5441352BCDF10B00385131 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A54413B2BCDF10B00385131 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A5441452BCDF10B00385131 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 3A5441292BCDF10900385131 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A588D0072BF6A0E500F9052A /* LoggerViewModel.swift in Sources */,
+ 3A8092F62BE9BB6B00AD667F /* TokenViewModel.swift in Sources */,
+ 3ACD90E12BE586BC00DABCE6 /* DavinciViewModel.swift in Sources */,
+ 3AE945742BF27224007B3381 /* LoginUtility.swift in Sources */,
+ A588D0062BF6A0E500F9052A /* StorageViewModel.swift in Sources */,
+ 3A8532332BE2D5D800F8619D /* LoginView.swift in Sources */,
+ 3A5441332BCDF10900385131 /* ContentView.swift in Sources */,
+ 3A8092F42BE99F0F00AD667F /* InputView.swift in Sources */,
+ 3AB1C9F42BD6C6F3003FCE3C /* LoginViewModel.swift in Sources */,
+ 3A3709962BEB038200AA7B51 /* AccessToken.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A5441392BCDF10B00385131 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3A5441422BCDF10B00385131 /* PingExampleTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 3A5441432BCDF10B00385131 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3A54414C2BCDF10B00385131 /* PingExampleUITests.swift in Sources */,
+ 3A54414E2BCDF10B00385131 /* PingExampleUITestsLaunchTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 3A54413F2BCDF10B00385131 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 3A54412C2BCDF10900385131 /* PingExample */;
+ targetProxy = 3A54413E2BCDF10B00385131 /* PBXContainerItemProxy */;
+ };
+ 3A5441492BCDF10B00385131 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 3A54412C2BCDF10900385131 /* PingExample */;
+ targetProxy = 3A5441482BCDF10B00385131 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 3A54414F2BCDF10B00385131 /* 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 = 16.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ OTHER_SWIFT_FLAGS = "";
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 3A5441502BCDF10B00385131 /* 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 = 16.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ OTHER_SWIFT_FLAGS = "";
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 3A5441522BCDF10B00385131 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"PingExample/Preview Content\"";
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_CFBundleDisplayName = Davinci;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 3A5441532BCDF10B00385131 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"PingExample/Preview Content\"";
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_CFBundleDisplayName = Davinci;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 3A5441552BCDF10B00385131 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExampleTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PingExample";
+ };
+ name = Debug;
+ };
+ 3A5441562BCDF10B00385131 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExampleTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PingExample";
+ };
+ name = Release;
+ };
+ 3A5441582BCDF10B00385131 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExampleUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = PingExample;
+ };
+ name = Debug;
+ };
+ 3A5441592BCDF10B00385131 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.PingExampleUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = PingExample;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 3A5441282BCDF10900385131 /* Build configuration list for PBXProject "PingExample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A54414F2BCDF10B00385131 /* Debug */,
+ 3A5441502BCDF10B00385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 3A5441512BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A5441522BCDF10B00385131 /* Debug */,
+ 3A5441532BCDF10B00385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 3A5441542BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExampleTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A5441552BCDF10B00385131 /* Debug */,
+ 3A5441562BCDF10B00385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 3A5441572BCDF10B00385131 /* Build configuration list for PBXNativeTarget "PingExampleUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3A5441582BCDF10B00385131 /* Debug */,
+ 3A5441592BCDF10B00385131 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 3A5441252BCDF10900385131 /* Project object */;
+}
diff --git a/SampleApps/PingExample/PingExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SampleApps/PingExample/PingExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/SampleApps/PingExample/PingExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/IDETemplateMacros.plist b/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/IDETemplateMacros.plist
new file mode 100644
index 0000000..16cf018
--- /dev/null
+++ b/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/IDETemplateMacros.plist
@@ -0,0 +1,17 @@
+
+
+
+
+ FILEHEADER
+
+// ___FILENAME___
+// ___PACKAGENAME___
+//
+// Copyright (c) ___YEAR___ Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+
diff --git a/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/xcschemes/PingExample.xcscheme b/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/xcschemes/PingExample.xcscheme
new file mode 100644
index 0000000..99ff12f
--- /dev/null
+++ b/SampleApps/PingExample/PingExample.xcodeproj/xcshareddata/xcschemes/PingExample.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SampleApps/PingExample/PingExample/AccessToken.swift b/SampleApps/PingExample/PingExample/AccessToken.swift
new file mode 100644
index 0000000..762a351
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/AccessToken.swift
@@ -0,0 +1,41 @@
+//
+// AccessToken.swift
+// PingExample
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+struct AccessTokenView: View {
+
+ @StateObject var accessToken = TokenViewModel()
+
+ var body: some View {
+ VStack {
+ ScrollView {
+ Text($accessToken.accessToken.wrappedValue)
+ .foregroundStyle(.secondary)
+ .padding(.horizontal)
+ .navigationTitle("AccessToken")
+ }
+ }
+ }
+}
+
+struct UserInfoView: View {
+
+ @StateObject var userInfoViewModel = UserInfoViewModel()
+
+ var body: some View {
+ ScrollView {
+ Text($userInfoViewModel.userInfo.wrappedValue)
+ .foregroundStyle(.secondary)
+ .padding(.horizontal)
+ .navigationTitle("User Info")
+ }
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/AccentColor.colorset/Contents.json b/SampleApps/PingExample/PingExample/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..514e171
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images" : [
+ {
+ "filename" : "Screenshot 2024-05-09-01 1.jpg",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Screenshot 2024-05-09-01 1.jpg b/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Screenshot 2024-05-09-01 1.jpg
new file mode 100644
index 0000000..8cfc7d4
Binary files /dev/null and b/SampleApps/PingExample/PingExample/Assets.xcassets/AppIcon.appiconset/Screenshot 2024-05-09-01 1.jpg differ
diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/Contents.json b/SampleApps/PingExample/PingExample/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Contents.json b/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Contents.json
new file mode 100644
index 0000000..88d4d3d
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "Screenshot 2024-05-09 at 12.00.43 PM.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git "a/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Screenshot 2024-05-09 at 12.00.43\342\200\257PM.png" "b/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Screenshot 2024-05-09 at 12.00.43\342\200\257PM.png"
new file mode 100644
index 0000000..9451c65
Binary files /dev/null and "b/SampleApps/PingExample/PingExample/Assets.xcassets/Logo.imageset/Screenshot 2024-05-09 at 12.00.43\342\200\257PM.png" differ
diff --git a/SampleApps/PingExample/PingExample/ContentView.swift b/SampleApps/PingExample/PingExample/ContentView.swift
new file mode 100644
index 0000000..13ada42
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/ContentView.swift
@@ -0,0 +1,161 @@
+import SwiftUI
+
+
+struct ContentView: View {
+
+
+ @State private var startDavinici = false
+
+ @State private var path: [String] = []
+
+ var body: some View {
+ NavigationStack(path: $path) {
+ List {
+ NavigationLink(value: "Davinci") {
+ Text("Launch Davinci")
+ }
+ NavigationLink(value: "Token") {
+ Text("Access Token")
+ }
+ NavigationLink(value: "User") {
+ Text("User Info")
+ }
+
+ NavigationLink(value: "Logout") {
+ Text("Logout")
+ }
+
+ NavigationLink(value: "Logger") {
+ Text("Logger")
+ }
+ NavigationLink(value: "Storage") {
+ Text("Storage")
+ }
+ }.navigationDestination(for: String.self) { item in
+ switch item {
+ case "Davinci":
+ DavinciView(path: $path)
+ case "Token":
+ AccessTokenView()
+ case "User":
+ UserInfoView()
+ case "Logout":
+ LogOutView(path: $path)
+ case "Logger":
+ LoggerView()
+ case "Storage":
+ StorageView()
+ default:
+ EmptyView()
+ }
+ }.navigationBarTitle("Ping Davinci")
+ }
+ }
+}
+
+
+struct LogOutView: View {
+
+ @Binding var path: [String]
+
+ @StateObject private var viewmodel = LogOutViewModel()
+
+ var body: some View {
+
+ Text("Logout")
+ .font(.title)
+ .navigationBarTitle("Logout", displayMode: .inline)
+
+ NextButton(title: "Procced to logout") {
+ Task {
+ await viewmodel.logout()
+ path.removeLast()
+ path.append("Davinci")
+ }
+ }
+
+ }
+}
+
+
+struct SecondTabView: View {
+ var body: some View {
+ Text("LogOut")
+ .font(.title)
+ .navigationBarTitle("LogOut", displayMode: .inline)
+
+ }
+}
+
+struct Register: View {
+ var body: some View {
+ Text("Register")
+ .font(.title)
+ .navigationBarTitle("Register", displayMode: .inline)
+ }
+}
+
+struct ForgotPassword: View {
+ var body: some View {
+ Text("ForgotPassword")
+ .font(.title)
+ .navigationBarTitle("ForgotPassword", displayMode: .inline)
+ }
+}
+
+struct LoggerView: View {
+ var loggerViewModel = LoggerViewModel()
+ var body: some View {
+ Text("This View is for testing Logger functionality.\nPlease check the Console Logs")
+ .font(.title3)
+ .multilineTextAlignment(.center)
+ .navigationBarTitle("Logger", displayMode: .inline)
+ .onAppear() {
+ loggerViewModel.setupLogger()
+ }
+ }
+}
+
+struct StorageView: View {
+ var storageViewModel = StorageViewModel()
+ var body: some View {
+ Text("This View is for testing Storage functionality.\nPlease check the Console Logs")
+ .font(.title3)
+ .multilineTextAlignment(.center)
+ .navigationBarTitle("Storage", displayMode: .inline)
+ .onAppear() {
+ Task {
+ await storageViewModel.setupMemoryStorage()
+ await storageViewModel.setupKeychainStorage()
+ }
+ }
+ }
+}
+
+@main
+struct MyApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
+
+struct ActivityIndicatorView: View {
+ @Binding var isAnimating: Bool
+ let style: UIActivityIndicatorView.Style
+ let color: Color
+
+ var body: some View {
+ if isAnimating {
+ VStack {
+ Spacer()
+ ProgressView()
+ .progressViewStyle(CircularProgressViewStyle(tint: color))
+ .padding()
+ Spacer()
+ }
+ .background(Color.black.opacity(0.4).ignoresSafeArea())
+ }
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/DavinciViewModel.swift b/SampleApps/PingExample/PingExample/DavinciViewModel.swift
new file mode 100644
index 0000000..4974883
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/DavinciViewModel.swift
@@ -0,0 +1,103 @@
+//
+// DavinciViewModel.swift
+// PingExample
+//
+// Copyright (c) 2024 Ping Identity. 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 PingDavinci
+import PingOidc
+import PingOrchestrate
+import PingLogger
+import PingStorage
+
+public let davinciStage = DaVinci.createDaVinci { config in
+ //config.debug = true
+
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "2dde00e2-3dd5-42b1-96e9-ad17e29f4bbd"
+ oidcValue.scopes = ["openid", "email", "address", "phone", "profile", "ttl"]
+ oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect"
+ oidcValue.discoveryEndpoint = "https://auth.test-one-pingone.com/0c6851ed-0f12-4c9a-a174-9b1bf8b438ae/as/.well-known/openid-configuration"
+ }
+}
+
+public let davinciTest = DaVinci.createDaVinci { config in
+ //config.debug = true
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1"
+ oidcValue.scopes = ["openid", "email", "address", "phone", "profile"]
+ oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect"
+ oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration"
+ }
+}
+
+public let davinciProd = DaVinci.createDaVinci { config in
+ //config.debug = true
+ config.module(OidcModule.config) { oidcValue in
+ oidcValue.clientId = "2dde00e2-3dd5-42b1-96e9-ad17e29f4bbd"
+ oidcValue.scopes = ["openid", "email", "address", "phone", "profile", "ttl"]
+ oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect"
+ oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration"
+ }
+}
+
+// Change this to Prod/Stage
+public let davinci = davinciProd
+
+class DavinciViewModel: ObservableObject {
+
+ @Published public var data: StateNode = StateNode()
+
+ @Published public var isLoading: Bool = false
+
+ init() {
+
+ Task {
+ await startDavinci()
+ }
+ }
+
+
+ private func startDavinci() async {
+
+ await MainActor.run {
+ isLoading = true
+ }
+
+ let node = await davinci.start()
+
+ await MainActor.run {
+ self.data = StateNode(currentNode: node, previousNode: node)
+ isLoading = false
+ }
+
+ }
+
+ public func next(node: Node) async {
+ await MainActor.run {
+ isLoading = true
+ }
+ if let nextNode = node as? ContinueNode {
+ let next = await nextNode.next()
+ await MainActor.run {
+ self.data = StateNode(currentNode: next, previousNode: node)
+ isLoading = false
+ }
+ }
+ }
+}
+
+class StateNode {
+ var currentNode: Node? = nil
+ var previousNode: Node? = nil
+
+ init(currentNode: Node? = nil, previousNode: Node? = nil) {
+ self.currentNode = currentNode
+ self.previousNode = previousNode
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/InputView.swift b/SampleApps/PingExample/PingExample/InputView.swift
new file mode 100644
index 0000000..c5f5f9e
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/InputView.swift
@@ -0,0 +1,110 @@
+//
+// InputView.swift
+// PingExample
+//
+// Copyright (c) 2024 Ping Identity. 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 PingDavinci
+
+struct InputView: View {
+ @State var text: String = ""
+ let placeholderString: String
+ var secureField: Bool = false
+ let field: any Collector
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ if secureField {
+ SecureField(placeholderString, text: $text)
+ .padding()
+ .background(Color.themeTextField)
+ .textContentType(.oneTimeCode)
+ .disableAutocorrection(true)
+ .autocapitalization(.none) // Set autocapitalization to none
+ .cornerRadius(20.0)
+ .shadow(radius: 10.0, x: 20, y: 10)
+ .onChange(of: text) { newValue in
+ if let field = field as? PasswordCollector {
+ field.value = newValue
+ }
+ }
+ } else {
+ TextField(placeholderString, text: $text)
+ .padding()
+ .background(Color.themeTextField)
+ .disableAutocorrection(true)
+ .textContentType(.oneTimeCode)
+ .autocapitalization(.none) // Set autocapitalization to none
+ .cornerRadius(20.0)
+ .shadow(radius: 10.0, x: 20, y: 10)
+ .onChange(of: text) { newValue in
+
+ if let field = field as? TextCollector {
+ field.value = newValue
+ }
+
+
+ }
+ }
+
+
+ }.padding([.leading, .trailing], 27.5)
+ }
+}
+
+struct InputButton: View {
+ let title: String
+ let field: any Collector
+ let action: () -> (Void)
+ var body: some View {
+ Button(action: {
+
+ if let field = field as? SubmitCollector {
+ field.value = field.key
+ }
+
+ action()
+ } ) {
+ Text(title)
+ .font(.headline)
+ .foregroundColor(.white)
+ .padding()
+ .frame(width: 300, height: 50)
+ .background(Color(red: 163.0/255.0, green: 19.0/255.0, blue: 0.0/255.0)) // Red color
+ .cornerRadius(15.0)
+ .shadow(radius: 10.0, x: 20, y: 10)
+ }
+
+ }
+}
+
+struct NextButton: View {
+ let title: String
+ let action: () -> (Void)
+ var body: some View {
+ Button(action: {
+ action()
+ } ) {
+ Text(title)
+ .font(.headline)
+ .foregroundColor(.white)
+ .padding()
+ .frame(width: 300, height: 50)
+ .background(Color.green)
+ .cornerRadius(15.0)
+ .shadow(radius: 10.0, x: 20, y: 10)
+ }
+
+ }
+}
+
+extension Color {
+ static var themeTextField: Color {
+ return Color(red: 220.0/255.0, green: 230.0/255.0, blue: 230.0/255.0, opacity: 1.0)
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/LoggerViewModel.swift b/SampleApps/PingExample/PingExample/LoggerViewModel.swift
new file mode 100644
index 0000000..91c749c
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/LoggerViewModel.swift
@@ -0,0 +1,98 @@
+//
+// LoggerViewModel.swift
+// PingExample
+//
+// Copyright (c) 2024 Ping Identity. 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 PingLogger
+
+class LoggerViewModel {
+
+ func setupLogger() {
+ // shared logger - by default is "none"
+ var sharedLogger = LogManager.logger
+ sharedLogger.d("sharedLogger Debug")
+ sharedLogger.i("sharedLogger Info")
+ sharedLogger.w("sharedLogger Warning", error: TestError.success)
+ sharedLogger.e("sharedLogger Error", error: TestError.failure)
+
+ // standard logger - messages of all levels should be displayed in the console...
+ let standardLogger = LogManager.standard
+ standardLogger.d("standardLogger Debug")
+ standardLogger.i("standardLogger Info")
+ standardLogger.w("standardLogger Warning", error: TestError.success)
+ standardLogger.e("standardLogger Error", error: TestError.failure)
+
+ // warning logger - only warning and error messages should be displayed...
+ let warningLogger = LogManager.warning
+ warningLogger.d("warningLogger Debug")
+ warningLogger.i("warningLogger Info")
+ warningLogger.w("warningLogger Warning", error: TestError.success)
+ warningLogger.e("warningLogger Error", error: TestError.failure)
+
+ // none logger - none of these messages will be displayed...
+ let noneLogger = LogManager.none
+ noneLogger.d("noneLogger Debug")
+ noneLogger.i("noneLogger Info")
+ noneLogger.w("noneLogger Warning", error: TestError.success)
+ noneLogger.e("noneLogger Error", error: TestError.failure)
+
+ // switch the shared logger to "standard" - the message from below will be displayed in the console
+ sharedLogger = LogManager.standard
+ sharedLogger.d("sharedLogger Debug")
+ sharedLogger.i("sharedLogger Info")
+ sharedLogger.w("sharedLogger Warning", error: TestError.success)
+ sharedLogger.e("sharedLogger Error", error: TestError.failure)
+
+ // test a custom logger
+ let customLogger = LogManager.customLogger
+ customLogger.d("customLogger Debug")
+ customLogger.i("customLogger Info")
+ customLogger.w("customLogger Warning", error: TestError.success)
+ customLogger.e("customLogger Error", error: TestError.failure)
+ }
+
+ enum TestError: Error {
+ case success
+ case failure
+ }
+}
+
+
+struct CustomLogger: Logger {
+
+ func i(_ message: String) {
+ print("\(message) (CustomLogger)")
+ }
+
+ func d(_ message: String) {
+ print("\(message) (CustomLogger)")
+ }
+
+ func w(_ message: String, error: Error?) {
+ if let error = error {
+ print("\(message): \(error) (CustomLogger)")
+ } else {
+ print("\(message) (CustomLogger)")
+ }
+ }
+
+ func e(_ message: String, error: Error?) {
+ if let error = error {
+ print("\(message): \(error) (CustomLogger)")
+ } else {
+ print("\(message) (CustomLogger)")
+ }
+ }
+}
+
+extension LogManager {
+ static var customLogger: Logger {
+ return CustomLogger()
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/LoginUtility.swift b/SampleApps/PingExample/PingExample/LoginUtility.swift
new file mode 100644
index 0000000..68eb2d8
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/LoginUtility.swift
@@ -0,0 +1,86 @@
+//
+// LoginView.swift
+// Davinci
+//
+// Copyright (c) 2024 Ping Identity. 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 SwiftUI
+
+struct LogoView: View {
+ var body: some View {
+ Image(systemName: "person.fill")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 100, height: 100)
+ .foregroundColor(.blue)
+ }
+}
+
+struct UsernameTextField: View {
+
+ @Binding var username: String
+
+ var body: some View {
+ TextField("Username", text: $username)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .autocapitalization(.none) // Set autocapitalization to none
+ .padding()
+ }
+}
+
+struct PasswordTextField: View {
+ @Binding var password: String
+
+ var body: some View {
+ SecureField("Password", text: $password)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .autocapitalization(.none) // Set autocapitalization to none
+ .padding()
+ }
+}
+
+struct LoginButton: View {
+ var action: () -> Void
+ var body: some View {
+ Button(action: action) {
+ Text("Login")
+ .padding()
+ .background(Color.blue)
+ .foregroundColor(.white)
+ .cornerRadius(10)
+ }
+ }
+}
+
+struct HavingTrouble: View {
+ var action: () -> Void
+ var body: some View {
+ Button(action: action) {
+ Text("Having Trouble Loggin In")
+ .padding()
+ .background(Color.blue)
+ .foregroundColor(.white)
+ .cornerRadius(10)
+ }
+ }
+}
+
+struct RegisterNow: View {
+ var action: () -> Void
+ var body: some View {
+ Button(action: action) {
+ Text("Register now")
+ .padding()
+ .background(Color.blue)
+ .foregroundColor(.white)
+ .cornerRadius(10)
+ }
+ }
+}
+
+
diff --git a/SampleApps/PingExample/PingExample/LoginView.swift b/SampleApps/PingExample/PingExample/LoginView.swift
new file mode 100644
index 0000000..2eb83c5
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/LoginView.swift
@@ -0,0 +1,173 @@
+//
+// LoginView.swift
+// PingExample
+//
+// Copyright (c) 2024 Ping Identity. 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 SwiftUI
+import PingOrchestrate
+import PingDavinci
+
+struct DavinciView: View {
+
+ @StateObject private var viewmodel = DavinciViewModel()
+ @Binding var path: [String]
+
+ var body: some View {
+ ZStack {
+ ScrollView {
+ VStack {
+ Spacer()
+ switch viewmodel.data.currentNode {
+ case let nextNode as ContinueNode:
+ ConnectorView(viewmodel: viewmodel, nextNode: nextNode)
+ case is SuccessNode:
+ VStack{}.onAppear {
+ path.removeLast()
+ path.append("Token")
+ }
+ case let failureNode as FailureNode:
+ if let nextNode = viewmodel.data.previousNode as? ContinueNode {
+ ConnectorView(viewmodel: viewmodel, nextNode: nextNode)
+ }
+
+ let apiError = failureNode.cause as? ApiError
+ switch apiError {
+ case .error(_, _, let message):
+ ErrorView(name: message)
+ default:
+ ErrorView(name: "unknown error")
+ }
+
+ case let errorNode as ErrorNode:
+ if let nextNode = viewmodel.data.previousNode as? ContinueNode {
+ ConnectorView(viewmodel: viewmodel, nextNode: nextNode)
+ }
+ ErrorView(name: "\(errorNode.message) :: \((errorNode.input["details"] as! [[String: Any]]).first!["message"])")
+ default:
+ EmptyView()
+ }
+ }
+ }
+
+ Spacer()
+
+ // Activity indicator
+ if viewmodel.isLoading {
+ Color.black.opacity(0.4)
+ .edgesIgnoringSafeArea(.all)
+
+ ProgressView()
+ .progressViewStyle(CircularProgressViewStyle())
+ .scaleEffect(4) // Increase spinner size if needed
+ .foregroundColor(.white) // Set spinner color
+ }
+ }
+ }
+}
+
+struct ConnectorView: View {
+
+ @ObservedObject var viewmodel: DavinciViewModel
+ public var nextNode: ContinueNode
+
+ var body: some View {
+ VStack {
+ Image("Logo").resizable().scaledToFill().frame(width: 100, height: 100)
+ .padding(.vertical, 32)
+ HeaderView(name: nextNode.name)
+ DescriptionView(name: nextNode.description)
+ NewLoginView(
+ davinciViewModel: viewmodel,
+ nextNode: nextNode, collectorsList: nextNode.collectors)
+ }
+ }
+}
+
+struct ErrorView: View {
+ var name: String = ""
+
+ var body: some View {
+ VStack {
+ Text("\(name)")
+ .foregroundColor(.red).padding(.top, 20)
+ }
+ }
+}
+
+struct HeaderView: View {
+ var name: String = ""
+ var body: some View {
+ VStack {
+ Text(name)
+ .font(.title)
+ }
+ }
+}
+
+struct DescriptionView: View {
+ var name: String = ""
+ var body: some View {
+ VStack {
+ Text(name)
+ .font(.subheadline)
+ }
+ }
+}
+
+struct NewLoginView: View {
+ // MARK: - Propertiers
+ @ObservedObject var davinciViewModel: DavinciViewModel
+
+ public var nextNode: ContinueNode
+
+ public var collectorsList: Collectors
+
+ // MARK: - View
+ var body: some View {
+
+ VStack {
+
+ ForEach(collectorsList, id: \.id) { field in
+
+ VStack {
+ if let text = field as? TextCollector {
+ InputView(text: text.value, placeholderString: text.label, field: text)
+ }
+
+ if let password = field as? PasswordCollector {
+ InputView(placeholderString: password.label, secureField: true, field: password)
+ }
+
+ if let submitButton = field as? SubmitCollector {
+ InputButton(title: submitButton.label, field: submitButton) {
+ Task {
+ await davinciViewModel.next(node: nextNode)
+ }
+ }
+ }
+
+ }.padding(.horizontal, 5).padding(.top, 20)
+
+
+ if let flowButton = field as? FlowCollector {
+ Button(action: {
+ flowButton.value = "action"
+ Task {
+ await davinciViewModel.next(node: nextNode)
+ }
+ }) {
+ Text(flowButton.label)
+ .foregroundColor(.black)
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/SampleApps/PingExample/PingExample/LoginViewModel.swift b/SampleApps/PingExample/PingExample/LoginViewModel.swift
new file mode 100644
index 0000000..04e9ce8
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/LoginViewModel.swift
@@ -0,0 +1,42 @@
+//
+// DavinciViewModel.swift
+// PingExample
+//
+// Copyright (c) 2024 Ping Identity. 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 SwiftUI
+import Observation
+import PingDavinci
+import PingOidc
+import PingOrchestrate
+
+class LoginViewModel: ObservableObject {
+
+ @Published public var isLoading: Bool = false
+
+ @ObservedObject var davinciViewModel: DavinciViewModel
+
+
+ init(
+ isLoading: Bool = false,
+ davinciViewModel: DavinciViewModel) {
+
+ self.isLoading = isLoading
+ self.davinciViewModel = davinciViewModel
+ }
+
+ public func next() async {
+ isLoading = true
+ if let nextNode = davinciViewModel.data.currentNode as? ContinueNode {
+ await davinciViewModel.next(node: nextNode)
+ isLoading = false
+ }
+ }
+
+}
+
diff --git a/SampleApps/PingExample/PingExample/Preview Content/Preview Assets.xcassets/Contents.json b/SampleApps/PingExample/PingExample/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/StorageViewModel.swift b/SampleApps/PingExample/PingExample/StorageViewModel.swift
new file mode 100644
index 0000000..dddcf97
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/StorageViewModel.swift
@@ -0,0 +1,40 @@
+//
+// StorageViewModel.swift
+// PingExample
+//
+// Copyright (c) 2024 Ping Identity. 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 PingStorage
+import PingLogger
+
+class StorageViewModel {
+
+ func setupMemoryStorage() async {
+ do {
+ let memoryStorage1 = MemoryStorage()
+ try await memoryStorage1.save(item: "Andy")
+ let storedValue1 = try await memoryStorage1.get()
+ LogManager.standard.i("Memory Storage value: \(storedValue1!)")
+ } catch {
+ LogManager.standard.e("", error: error)
+ }
+
+ }
+
+
+ func setupKeychainStorage() async {
+ do {
+ let keychainStorage = KeychainStorage(account: "token", encryptor: SecuredKeyEncryptor() ?? NoEncryptor())
+ try await keychainStorage.save(item: "Jey")
+ let storedValue = try await keychainStorage.get()
+ LogManager.standard.i("Kechain Storage value: \(storedValue!)")
+ } catch {
+ LogManager.standard.e("", error: error)
+ }
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/TokenViewModel.swift b/SampleApps/PingExample/PingExample/TokenViewModel.swift
new file mode 100644
index 0000000..8bf970d
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/TokenViewModel.swift
@@ -0,0 +1,87 @@
+//
+// TokenViewModel.swift
+// PingExample
+//
+// Copyright (c) 2024 Ping Identity. 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 PingLogger
+
+class TokenViewModel: ObservableObject {
+
+ @Published var accessToken: String = ""
+
+
+ init() {
+ Task {
+ await accessToken()
+ }
+ }
+
+ func accessToken() async {
+ let token = await davinci.user()?.token()
+ switch token {
+ case .success(let accessToken):
+ await MainActor.run {
+ self.accessToken = String(describing: accessToken)
+ }
+ LogManager.standard.i("AccessToken: \(self.accessToken)")
+ case .failure(let error):
+ await MainActor.run {
+ self.accessToken = "Error: \(error.localizedDescription)"
+ }
+ LogManager.standard.e("", error: error)
+ case .none:
+ break
+ }
+
+ }
+}
+
+class UserInfoViewModel: ObservableObject {
+
+ @Published var userInfo: String = ""
+
+ init() {
+ Task {
+ await fetchUserInfo()
+ }
+ }
+
+ func fetchUserInfo() async {
+ let userInfo = await davinci.user()?.userinfo(cache: false)
+ switch userInfo {
+ case .success(let userInfoDictionary):
+ await MainActor.run {
+ var userInfoDescription = ""
+ userInfoDictionary.forEach { userInfoDescription += "\($0): \($1)\n" }
+ self.userInfo = userInfoDescription
+ }
+ LogManager.standard.i("UserInfo: \(String(describing: self.userInfo))")
+ case .failure(let error):
+ await MainActor.run {
+ self.userInfo = "Error: \(error.localizedDescription)"
+ }
+ LogManager.standard.e("", error: error)
+ case .none:
+ break
+ }
+ }
+}
+
+class LogOutViewModel: ObservableObject {
+
+ @Published var logout: String = ""
+
+ func logout() async {
+ await davinci.user()?.logout()
+ await MainActor.run {
+ logout = "Logout completed"
+ }
+
+ }
+}
diff --git a/SampleApps/PingExample/PingExample/UserProfileViewModel.swift b/SampleApps/PingExample/PingExample/UserProfileViewModel.swift
new file mode 100644
index 0000000..e0b0046
--- /dev/null
+++ b/SampleApps/PingExample/PingExample/UserProfileViewModel.swift
@@ -0,0 +1,28 @@
+//
+// UserProfileViewModel.swift
+// PingExample
+//
+// Created by jey periyasamy on 5/8/24.
+//
+
+import Foundation
+
+class UserProfileViewModel: ObservableObject {
+
+ @Published var userProfile: String = ""
+
+ init() {
+ Task {
+ await self.accessToken()
+ }
+ }
+
+ func accessToken() async {
+// let foo = await davinci.user()?.userinfo(cache: <#T##Bool#>)
+// await MainActor.run {
+// accessToken = foo.debugDescription
+// }
+//
+// print("AccessToken ----->\(String(describing: foo))")
+ }
+}
diff --git a/SampleApps/PingExample/PingExampleTests/PingExampleTests.swift b/SampleApps/PingExample/PingExampleTests/PingExampleTests.swift
new file mode 100644
index 0000000..4cd132a
--- /dev/null
+++ b/SampleApps/PingExample/PingExampleTests/PingExampleTests.swift
@@ -0,0 +1,39 @@
+//
+// PingExampleTests.swift
+// PingExampleTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+import XCTest
+@testable import PingExample
+
+final class PingExampleTests: XCTestCase {
+
+ override func setUpWithError() throws {
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+ }
+
+ override func tearDownWithError() throws {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ func testExample() throws {
+ // This is an example of a functional test case.
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
+ // Any test you write for XCTest can be annotated as throws and async.
+ // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
+ // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
+ }
+
+ func testPerformanceExample() throws {
+ // This is an example of a performance test case.
+ self.measure {
+ // Put the code you want to measure the time of here.
+ }
+ }
+
+}
diff --git a/SampleApps/PingExample/PingExampleUITests/PingExampleUITests.swift b/SampleApps/PingExample/PingExampleUITests/PingExampleUITests.swift
new file mode 100644
index 0000000..022dcf3
--- /dev/null
+++ b/SampleApps/PingExample/PingExampleUITests/PingExampleUITests.swift
@@ -0,0 +1,44 @@
+//
+// PingExampleUITests.swift
+// PingExampleUITests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+import XCTest
+
+final class PingExampleUITests: 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.
+ }
+
+ 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.
+ }
+
+ func testLaunchPerformance() throws {
+ if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
+ // This measures how long it takes to launch your application.
+ measure(metrics: [XCTApplicationLaunchMetric()]) {
+ XCUIApplication().launch()
+ }
+ }
+ }
+}
diff --git a/SampleApps/PingExample/PingExampleUITests/PingExampleUITestsLaunchTests.swift b/SampleApps/PingExample/PingExampleUITests/PingExampleUITestsLaunchTests.swift
new file mode 100644
index 0000000..209ada0
--- /dev/null
+++ b/SampleApps/PingExample/PingExampleUITests/PingExampleUITestsLaunchTests.swift
@@ -0,0 +1,35 @@
+//
+// PingExampleUITestsLaunchTests.swift
+// PingExampleUITests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+import XCTest
+
+final class PingExampleUITestsLaunchTests: XCTestCase {
+
+ override class var runsForEachTargetApplicationUIConfiguration: Bool {
+ true
+ }
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ }
+
+ func testLaunch() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Insert steps here to perform after app launch but before taking a screenshot,
+ // such as logging into a test account or navigating somewhere in the app
+
+ let attachment = XCTAttachment(screenshot: app.screenshot())
+ attachment.name = "Launch Screen"
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ }
+}
diff --git a/Storage/README.md b/Storage/README.md
new file mode 100644
index 0000000..9a57dc6
--- /dev/null
+++ b/Storage/README.md
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+# PingStorage SDK
+
+The PingStorage SDK provides a flexible storage interface and a set of common storage solutions for the Ping SDKs.
+
+## Integrating the SDK into your project
+
+Use Cocoapods or Swift Package Manger
+
+## How to Use the SDK
+
+### Creating and Using a Storage Instance
+
+To create a storage instance and use it to persist and retrieve data, follow the example below:
+
+```swift
+// Define the data type that you want to persist
+struct Dog: Codable {
+ let name: String
+ let type: String
+}
+
+ let storage = KeychainStorage(account: "myId") // Create the storage
+ try? await storage.save(item: Dog(name: "Lucky", type: "Golden Retriever")) // Persist the item
+ let storedData = try? await storage.get() // Retrieve the item
+```
+
+Keychain is a storage solution that
+uses iOS Keychain to store data securely.
+
+### Enabling Cache for the Storage
+
+You can enable cache for the storage as follows, by default cache is disabled:
+
+```swift
+ let storage = KeychainStorage(account: "myId", cacheable: true) // Create the Storage with cache enabled
+```
+
+### Adding Encryption to the Storage
+
+You can add encryption by specifying the encryptor (`Encryptor` instance) as follows, by default `NoEncryptor` is used:
+
+```swift
+ let storage = KeychainStorage(account: "myId", encryptor: SecuredKeyEncryptor() ?? NoEncryptor(), cacheable: true) // Create the Storage with `SecuredKeyEncryptor`
+```
+
+You can create your custom encryptor by implementing the `Encryptor` protocol:
+
+```swift
+struct MyEncryptor: Encryptor {
+ func encrypt(data: Data) async throws -> Data {
+ // Implement the encryption logic
+ }
+
+ func decrypt(data: Data) async throws -> Data {
+ // Implement the decryption logic
+ }
+}
+```
+
+### Creating a Custom Storage
+
+You can create a custom storage by implementing the `Storage` interface. This could be useful for creating
+file-based storage, cloud storage, etc. Here is an example of creating a custom memory storage:
+
+```swift
+public class CustomStorage: Storage {
+ private var data: T?
+
+ public func save(item: T) async throws {
+ data = item
+ }
+
+ public func get() async throws -> T? {
+ return data
+ }
+
+ public func delete() async throws {
+ data = nil
+ }
+
+}
+
+public class CustomStorageDelegate: StorageDelegate {
+ public init(cacheable: Bool = false) {
+ super.init(delegate: CustomStorage(), cacheable: cacheable)
+ }
+}
+
+```
+
+## Available Storage Solutions
+
+The PingStorage SDK provides the following storage solutions:
+
+| Storage | Description |
+|------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
+| KeychainStorage | Storage that stores data in iOS Keychain. |
+| MemoryStorage | Storage that stores data in memory. |
diff --git a/Storage/Storage.xcodeproj/project.pbxproj b/Storage/Storage.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..82c681f
--- /dev/null
+++ b/Storage/Storage.xcodeproj/project.pbxproj
@@ -0,0 +1,538 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 95E10E5D2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E10E5C2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift */; };
+ A509667B2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A509667A2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift */; };
+ A509667D2C45C4E000A4E5B5 /* CustomEncryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A509667C2C45C4E000A4E5B5 /* CustomEncryptorTests.swift */; };
+ A54BF44C2BED188000CD24D4 /* SecuredKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF44B2BED188000CD24D4 /* SecuredKey.swift */; };
+ A54BF49F2BF2F04E00CD24D4 /* SecuredKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF49E2BF2F04E00CD24D4 /* SecuredKeyTests.swift */; };
+ A54BF4E52BF39DC200CD24D4 /* MemoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF4E42BF39DC200CD24D4 /* MemoryStorageTests.swift */; };
+ A54BF4E72BF39E0900CD24D4 /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF4E62BF39E0900CD24D4 /* KeychainStorageTests.swift */; };
+ A54BF4E92BF39F8000CD24D4 /* StorageDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54BF4E82BF39F8000CD24D4 /* StorageDelegateTests.swift */; };
+ A57613952C45ABA200EC0E3F /* Encryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57613942C45ABA200EC0E3F /* Encryptor.swift */; };
+ A57613972C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57613962C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift */; };
+ A588CFA72BF654FA00F9052A /* CustomStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A588CFA62BF654FA00F9052A /* CustomStorageTests.swift */; };
+ A5A712422CAC51D800B7DD58 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A5A712412CAC51D700B7DD58 /* PrivacyInfo.xcprivacy */; };
+ A5A797052BE1782F004D0F2D /* PingStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5A796FA2BE1782E004D0F2D /* PingStorage.framework */; };
+ A5A7970B2BE1782F004D0F2D /* Storage.h in Headers */ = {isa = PBXBuildFile; fileRef = A5A796FD2BE1782E004D0F2D /* Storage.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ A5A7971E2BE17C95004D0F2D /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A7971D2BE17C95004D0F2D /* Storage.swift */; };
+ A5A797392BE18BE3004D0F2D /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A797382BE18BE3004D0F2D /* MemoryStorage.swift */; };
+ A5A7973B2BE2A035004D0F2D /* StorageDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A7973A2BE2A035004D0F2D /* StorageDelegate.swift */; };
+ A5A7974D2BE33279004D0F2D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A7974C2BE33279004D0F2D /* KeychainStorage.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ A5A797062BE1782F004D0F2D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = A5A796F12BE1782E004D0F2D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = A5A796F92BE1782E004D0F2D;
+ remoteInfo = PingStorage;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ 95E10E5C2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedKeychainStorageTests.swift; sourceTree = ""; };
+ A509667A2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecuredKeyEncryptorTests.swift; sourceTree = ""; };
+ A509667C2C45C4E000A4E5B5 /* CustomEncryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEncryptorTests.swift; sourceTree = ""; };
+ A54BF44B2BED188000CD24D4 /* SecuredKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecuredKey.swift; sourceTree = ""; };
+ A54BF49E2BF2F04E00CD24D4 /* SecuredKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecuredKeyTests.swift; sourceTree = ""; };
+ A54BF4E42BF39DC200CD24D4 /* MemoryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorageTests.swift; sourceTree = ""; };
+ A54BF4E62BF39E0900CD24D4 /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = ""; };
+ A54BF4E82BF39F8000CD24D4 /* StorageDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageDelegateTests.swift; sourceTree = ""; };
+ A57613942C45ABA200EC0E3F /* Encryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encryptor.swift; sourceTree = ""; };
+ A57613962C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecuredKeyEncryptor.swift; sourceTree = ""; };
+ A588CFA62BF654FA00F9052A /* CustomStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStorageTests.swift; sourceTree = ""; };
+ A5A712412CAC51D700B7DD58 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
+ A5A796FA2BE1782E004D0F2D /* PingStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PingStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A5A796FD2BE1782E004D0F2D /* Storage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Storage.h; sourceTree = ""; };
+ A5A797042BE1782F004D0F2D /* StorageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StorageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ A5A7971D2BE17C95004D0F2D /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; };
+ A5A797382BE18BE3004D0F2D /* MemoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorage.swift; sourceTree = ""; };
+ A5A7973A2BE2A035004D0F2D /* StorageDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageDelegate.swift; sourceTree = ""; };
+ A5A7974C2BE33279004D0F2D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ A5A796F72BE1782E004D0F2D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A5A797012BE1782F004D0F2D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A797052BE1782F004D0F2D /* PingStorage.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ A5A796F02BE1782E004D0F2D = {
+ isa = PBXGroup;
+ children = (
+ A5A796FC2BE1782E004D0F2D /* Storage */,
+ A5A797082BE1782F004D0F2D /* StorageTests */,
+ A5A796FB2BE1782E004D0F2D /* Products */,
+ );
+ sourceTree = "";
+ };
+ A5A796FB2BE1782E004D0F2D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A5A796FA2BE1782E004D0F2D /* PingStorage.framework */,
+ A5A797042BE1782F004D0F2D /* StorageTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ A5A796FC2BE1782E004D0F2D /* Storage */ = {
+ isa = PBXGroup;
+ children = (
+ A5A712412CAC51D700B7DD58 /* PrivacyInfo.xcprivacy */,
+ A5A796FD2BE1782E004D0F2D /* Storage.h */,
+ A57613942C45ABA200EC0E3F /* Encryptor.swift */,
+ A5A7974C2BE33279004D0F2D /* KeychainStorage.swift */,
+ A5A797382BE18BE3004D0F2D /* MemoryStorage.swift */,
+ A54BF44B2BED188000CD24D4 /* SecuredKey.swift */,
+ A57613962C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift */,
+ A5A7971D2BE17C95004D0F2D /* Storage.swift */,
+ A5A7973A2BE2A035004D0F2D /* StorageDelegate.swift */,
+ );
+ path = Storage;
+ sourceTree = "";
+ };
+ A5A797082BE1782F004D0F2D /* StorageTests */ = {
+ isa = PBXGroup;
+ children = (
+ A509667C2C45C4E000A4E5B5 /* CustomEncryptorTests.swift */,
+ A588CFA62BF654FA00F9052A /* CustomStorageTests.swift */,
+ A54BF4E62BF39E0900CD24D4 /* KeychainStorageTests.swift */,
+ 95E10E5C2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift */,
+ A54BF4E42BF39DC200CD24D4 /* MemoryStorageTests.swift */,
+ A54BF49E2BF2F04E00CD24D4 /* SecuredKeyTests.swift */,
+ A509667A2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift */,
+ A54BF4E82BF39F8000CD24D4 /* StorageDelegateTests.swift */,
+ );
+ path = StorageTests;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+ A5A796F52BE1782E004D0F2D /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A7970B2BE1782F004D0F2D /* Storage.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+ A5A796F92BE1782E004D0F2D /* PingStorage */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = A5A7970E2BE1782F004D0F2D /* Build configuration list for PBXNativeTarget "PingStorage" */;
+ buildPhases = (
+ A5A796F52BE1782E004D0F2D /* Headers */,
+ A5A796F62BE1782E004D0F2D /* Sources */,
+ A5A796F72BE1782E004D0F2D /* Frameworks */,
+ A5A796F82BE1782E004D0F2D /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PingStorage;
+ productName = PingStorage;
+ productReference = A5A796FA2BE1782E004D0F2D /* PingStorage.framework */;
+ productType = "com.apple.product-type.framework";
+ };
+ A5A797032BE1782F004D0F2D /* StorageTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = A5A797112BE1782F004D0F2D /* Build configuration list for PBXNativeTarget "StorageTests" */;
+ buildPhases = (
+ A5A797002BE1782F004D0F2D /* Sources */,
+ A5A797012BE1782F004D0F2D /* Frameworks */,
+ A5A797022BE1782F004D0F2D /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ A5A797072BE1782F004D0F2D /* PBXTargetDependency */,
+ );
+ name = StorageTests;
+ productName = PingStorageTests;
+ productReference = A5A797042BE1782F004D0F2D /* StorageTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ A5A796F12BE1782E004D0F2D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1530;
+ LastUpgradeCheck = 1530;
+ TargetAttributes = {
+ A5A796F92BE1782E004D0F2D = {
+ CreatedOnToolsVersion = 15.3;
+ };
+ A5A797032BE1782F004D0F2D = {
+ CreatedOnToolsVersion = 15.3;
+ };
+ };
+ };
+ buildConfigurationList = A5A796F42BE1782E004D0F2D /* Build configuration list for PBXProject "Storage" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = A5A796F02BE1782E004D0F2D;
+ productRefGroup = A5A796FB2BE1782E004D0F2D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ A5A796F92BE1782E004D0F2D /* PingStorage */,
+ A5A797032BE1782F004D0F2D /* StorageTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ A5A796F82BE1782E004D0F2D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A712422CAC51D800B7DD58 /* PrivacyInfo.xcprivacy in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A5A797022BE1782F004D0F2D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ A5A796F62BE1782E004D0F2D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A5A797392BE18BE3004D0F2D /* MemoryStorage.swift in Sources */,
+ A57613972C45ACFA00EC0E3F /* SecuredKeyEncryptor.swift in Sources */,
+ A54BF44C2BED188000CD24D4 /* SecuredKey.swift in Sources */,
+ A5A7973B2BE2A035004D0F2D /* StorageDelegate.swift in Sources */,
+ A5A7974D2BE33279004D0F2D /* KeychainStorage.swift in Sources */,
+ A57613952C45ABA200EC0E3F /* Encryptor.swift in Sources */,
+ A5A7971E2BE17C95004D0F2D /* Storage.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A5A797002BE1782F004D0F2D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A509667B2C45C19500A4E5B5 /* SecuredKeyEncryptorTests.swift in Sources */,
+ A54BF4E92BF39F8000CD24D4 /* StorageDelegateTests.swift in Sources */,
+ A54BF4E72BF39E0900CD24D4 /* KeychainStorageTests.swift in Sources */,
+ A509667D2C45C4E000A4E5B5 /* CustomEncryptorTests.swift in Sources */,
+ A54BF4E52BF39DC200CD24D4 /* MemoryStorageTests.swift in Sources */,
+ 95E10E5D2CEEA80B0089FF5E /* EncryptedKeychainStorageTests.swift in Sources */,
+ A54BF49F2BF2F04E00CD24D4 /* SecuredKeyTests.swift in Sources */,
+ A588CFA72BF654FA00F9052A /* CustomStorageTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ A5A797072BE1782F004D0F2D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = A5A796F92BE1782E004D0F2D /* PingStorage */;
+ targetProxy = A5A797062BE1782F004D0F2D /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ A5A7970C2BE1782F004D0F2D /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ 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";
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Debug;
+ };
+ A5A7970D2BE1782F004D0F2D /* 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;
+ CURRENT_PROJECT_VERSION = 1;
+ 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 = 13.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Release;
+ };
+ A5A7970F2BE1782F004D0F2D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Storage;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_INSTALL_OBJC_HEADER = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ A5A797102BE1782F004D0F2D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MARKETING_VERSION = "1.0.0";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ OTHER_SWIFT_FLAGS = "-no-verify-emitted-module-interface";
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.Storage;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_INSTALL_OBJC_HEADER = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ A5A797122BE1782F004D0F2D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.StorageTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Debug;
+ };
+ A5A797132BE1782F004D0F2D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 9QSE66762D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.pingidentity.StorageTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PingTestHost.app/PingTestHost";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ A5A796F42BE1782E004D0F2D /* Build configuration list for PBXProject "Storage" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A5A7970C2BE1782F004D0F2D /* Debug */,
+ A5A7970D2BE1782F004D0F2D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ A5A7970E2BE1782F004D0F2D /* Build configuration list for PBXNativeTarget "PingStorage" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A5A7970F2BE1782F004D0F2D /* Debug */,
+ A5A797102BE1782F004D0F2D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ A5A797112BE1782F004D0F2D /* Build configuration list for PBXNativeTarget "StorageTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A5A797122BE1782F004D0F2D /* Debug */,
+ A5A797132BE1782F004D0F2D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = A5A796F12BE1782E004D0F2D /* Project object */;
+}
diff --git a/Storage/Storage.xcodeproj/xcshareddata/xcschemes/Storage.xcscheme b/Storage/Storage.xcodeproj/xcshareddata/xcschemes/Storage.xcscheme
new file mode 100644
index 0000000..f2681d7
--- /dev/null
+++ b/Storage/Storage.xcodeproj/xcshareddata/xcschemes/Storage.xcscheme
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Storage/Storage/Encryptor.swift b/Storage/Storage/Encryptor.swift
new file mode 100644
index 0000000..92ba741
--- /dev/null
+++ b/Storage/Storage/Encryptor.swift
@@ -0,0 +1,50 @@
+//
+// Encryptor.swift
+// PingStorage
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// A protocol that defines methods for encrypting and decrypting data.
+public protocol Encryptor {
+ /// Encrypts the given data.
+ /// - Parameter data: The data to encrypt.
+ /// - Returns: The encrypted data.
+ /// - Throws: An error if encryption fails.
+ func encrypt(data: Data) async throws -> Data
+
+ /// Decrypts the given data.
+ /// - Parameter data: The data to decrypt.
+ /// - Returns: The decrypted data.
+ /// - Throws: An error if decryption fails.
+ func decrypt(data: Data) async throws -> Data
+}
+
+
+/// A struct that provides no encryption.
+public struct NoEncryptor: Encryptor {
+ /// Initializes a new instance of `NoEncryptor`.
+ public init() {}
+
+ /// Returns the given data without performing any encryption.
+ ///
+ /// - Parameter data: The data to "encrypt".
+ /// - Returns: The same data that was provided.
+ public func encrypt(data: Data) async throws -> Data {
+ return data
+ }
+
+ /// Returns the given data without performing any decryption.
+ ///
+ /// - Parameter data: The data to "decrypt".
+ /// - Returns: The same data that was provided.
+ public func decrypt(data: Data) async throws -> Data {
+ return data
+ }
+}
diff --git a/Storage/Storage/KeychainStorage.swift b/Storage/Storage/KeychainStorage.swift
new file mode 100644
index 0000000..4cca3b4
--- /dev/null
+++ b/Storage/Storage/KeychainStorage.swift
@@ -0,0 +1,125 @@
+//
+// KeychainStorage.swift
+// PingStorage
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// A storage for storing `Codable` objects in the Keychain
+public class Keychain: Storage {
+ private var account: String
+ private let service: String = "com.pingidentity.keychainService"
+ private let encryptor: Encryptor
+
+ /// Initializer for Keychain
+ /// - Parameters:
+ /// - account: String indicating the item's account(key) name.
+ /// - encryptor: Encryptor for encrypting stored data. Default value is `NoEncryptor()`
+ public init(account: String, encryptor: Encryptor = NoEncryptor()) {
+ self.account = account
+ self.encryptor = encryptor
+ }
+
+ /// Saves the given item in the keychain.
+ /// - Parameter item: The item to save.
+ public func save(item: T) async throws {
+ let data = try JSONEncoder().encode(item)
+ var query = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrAccount as String: account,
+ kSecAttrService as String: service,
+ kSecValueData as String: data
+ ] as [String: Any]
+
+ query[kSecValueData as String] = try await encryptor.encrypt(data: data)
+
+ SecItemDelete(query as CFDictionary) // Remove any existing item
+ let status = SecItemAdd(query as CFDictionary, nil)
+
+ guard status == errSecSuccess else {
+ throw KeychainError.unableToSave
+ }
+ }
+
+ /// Retrieves the item from the keychain.
+ /// - Returns: The item if it exists, `nil` otherwise.
+ public func get() async throws -> T? {
+ let query = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrAccount as String: account,
+ kSecAttrService as String: service,
+ kSecMatchLimit as String: kSecMatchLimitOne,
+ kSecReturnData as String: kCFBooleanTrue!
+ ] as [String: Any]
+
+ var item: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
+
+ guard status == errSecSuccess, let data = item as? Data else {
+ return nil
+ }
+
+ return try JSONDecoder().decode(T.self, from: try await encryptor.decrypt(data: data))
+ }
+
+ /// Deletes the item from memory.
+ public func delete() async throws {
+ let query = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrAccount as String: account,
+ kSecAttrService as String: service
+ ] as [String: Any]
+
+ let status = SecItemDelete(query as CFDictionary)
+ guard status == errSecSuccess else {
+ throw KeychainError.unableToDelete
+ }
+ }
+}
+
+
+/// `KeychainError` represents errors that can occur while interacting with the keychain.
+public enum KeychainError: LocalizedError {
+ case unableToSave
+ case unableToRetrieve
+ case unableToDelete
+
+ /// A localized message describing what error occurred.
+ public var errorMessage: String {
+ switch self {
+ case .unableToSave:
+ return "Uanble to save to the keychain"
+ case .unableToRetrieve:
+ return "Uanble to retrieve from the keychain"
+ case .unableToDelete:
+ return "Unable to delete from the kechain"
+ }
+ }
+}
+
+
+/// `KeychainStorage` is a generic class that conforms to the `StorageDelegate` protocol, providing a secure storage solution by leveraging the keychain.
+/// It is designed to store, retrieve, and manage objects of type `T`, where `T` must conform to the `Codable` protocol. This requirement ensures that the objects can be easily encoded and decoded for secure storage in the keychain.
+///
+/// - Parameter T: The type of the objects to be stored in the keychain. Must conform to `Codable`.
+public class KeychainStorage: StorageDelegate {
+ /// Initializes a new instance of `KeychainStorage`.
+ ///
+ /// This initializer configures a `KeychainStorage` instance with a specified account and security settings.
+ /// It allows storing data securely in the keychain using the provided account identifier. T
+ ///
+ /// - Parameters:
+ /// - account: A `String` identifying the keychain account under which the data will be stored. This is used
+ /// to differentiate between different sets of data within the keychain.
+ /// - encryptor: An `Encryptor` instance for encrypting/decrypting the stored data. Default value is `NoEncryptor()`
+ /// - cacheable: A `Bool` indicating whether the stored data should be cached. Defaults to `false`.
+ public init(account: String, encryptor: Encryptor = NoEncryptor(), cacheable: Bool = false) {
+ super.init(delegate: Keychain(account: account, encryptor: encryptor), cacheable: cacheable)
+ }
+}
diff --git a/Storage/Storage/MemoryStorage.swift b/Storage/Storage/MemoryStorage.swift
new file mode 100644
index 0000000..7faef19
--- /dev/null
+++ b/Storage/Storage/MemoryStorage.swift
@@ -0,0 +1,56 @@
+//
+// MemoryStorage.swift
+// PingStorage
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// A storage for storing objects in memory, where `T` is the type of the object to be stored.
+public class Memory: Storage {
+ private var data: T?
+
+ /// Saves the given item in memory.
+ /// - Parameter item: The item to save.
+ public func save(item: T) async throws {
+ data = item
+ }
+
+ /// Retrieves the item from memory.
+ /// - Returns: The item if it exists, `nil` otherwise.
+ public func get() async throws -> T? {
+ return data
+ }
+
+ /// Deletes the item from memory.
+ public func delete() async throws {
+ data = nil
+ }
+}
+
+
+/// `MemoryStorage` provides an in-memory storage solution for objects of type `T`.
+/// It conforms to the `StorageDelegate` protocol, enabling it to interact seamlessly with other components expecting a storage delegate.
+/// This class is ideal for temporary storage where persistence across app launches is not required.
+///
+/// The generic type `T` must conform to `Codable` to ensure that objects can be encoded and decoded when written to and read from memory, respectively.
+///
+/// - Parameter T: The type of the objects to be stored. Must conform to `Codable`.
+public class MemoryStorage: StorageDelegate {
+ /// Initializes a new instance of `MemoryStorage`.
+ ///
+ /// This initializer creates a `MemoryStorage` instance that acts as a delegate for an in-memory storage
+ /// mechanism. It allows for the optional caching of data based on the `cacheable` parameter.
+ ///
+ /// - Parameter cacheable: A Boolean value indicating whether the stored data should be cached. Defaults to `false`,
+ /// which means that caching is not enabled by default. When set to `true`, it enables caching
+ /// based on the implementation details of the `Memory` storage strategy.
+ public init(cacheable: Bool = false) {
+ super.init(delegate: Memory(), cacheable: cacheable)
+ }
+}
diff --git a/Storage/Storage/PrivacyInfo.xcprivacy b/Storage/Storage/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..fcfc9b9
--- /dev/null
+++ b/Storage/Storage/PrivacyInfo.xcprivacy
@@ -0,0 +1,10 @@
+
+
+
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTracking
+
+
+
diff --git a/Storage/Storage/SecuredKey.swift b/Storage/Storage/SecuredKey.swift
new file mode 100644
index 0000000..464832a
--- /dev/null
+++ b/Storage/Storage/SecuredKey.swift
@@ -0,0 +1,164 @@
+//
+// SecuredKey.swift
+// PingStorage
+//
+// Copyright (c) 2024 Ping Identity. 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 LocalAuthentication
+import CryptoKit
+
+/// `SecuredKey` is a representation of Secure Enclave keypair and performing PKI using Secure Enclave
+public struct SecuredKey {
+ /// Private Key of SecuredKey
+ fileprivate var privateKey: SecKey
+ /// Public Key of SecuredKey
+ fileprivate var publicKey: SecKey
+ /// Algorithm to be used for encryption/decryption using SecuredKey
+ fileprivate let oldAlgorithm: SecKeyAlgorithm = .eciesEncryptionCofactorX963SHA256AESGCM
+
+ /// Validates whether SecuredKey using Secure Enclave is available on the device or not
+ public static func isAvailable() -> Bool {
+ return SecureEnclave.isAvailable
+ }
+
+ /// Initializes `SecuredKey` object with designated service; `SecuredKey` may return `nil` if it failed to generate keypair
+ /// - Parameter applicationTag: Unique identifier for SecuredKey
+ public init?(applicationTag: String) {
+ guard SecuredKey.isAvailable() else {
+ return nil
+ }
+
+ // If SecuredKey already exists, return from the storage
+ if let privateKey = SecuredKey.readKey(applicationTag: applicationTag) {
+ self.privateKey = privateKey
+ }
+ else {
+ // Otherwise, generate new keypair
+ do {
+ self.privateKey = try SecuredKey.generateKey(applicationTag: applicationTag, accessGroup: nil, accessibility: kSecAttrAccessibleAfterFirstUnlock)
+ }
+ catch {
+ return nil
+ }
+ }
+
+ // Copy the public key from the private key
+ if let publicKey = SecKeyCopyPublicKey(self.privateKey) {
+ self.publicKey = publicKey
+ }
+ else {
+ return nil
+ }
+ }
+
+ /// Retrieves private key with given 'ApplicationTag'
+ /// - Parameter applicationTag: Application Tag string value for private key
+ static func readKey(applicationTag: String, accessGroup: String? = nil) -> SecKey? {
+ var query = [String: Any]()
+ query[String(kSecClass)] = kSecClassKey
+ query[String(kSecAttrKeyType)] = String(kSecAttrKeyTypeEC)
+ query[String(kSecReturnRef)] = true
+ query[String(kSecAttrApplicationTag)] = applicationTag
+
+ if let accessGroup = accessGroup {
+ query[String(kSecAttrAccessGroup)] = accessGroup
+ }
+
+ var item: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
+ guard status == errSecSuccess else {
+ return nil
+ }
+ return (item as! SecKey)
+ }
+
+ /// Generates private key with given 'ApplicationTag'
+ /// - Parameter applicationTag: Application Tag string value for private key
+ static func generateKey(applicationTag: String, accessGroup: String? = nil, accessibility: CFString) throws -> SecKey {
+ var query = [String: Any]()
+
+ query[String(kSecAttrKeyType)] = String(kSecAttrKeyTypeEC)
+ query[String(kSecAttrKeySizeInBits)] = 256
+
+ if let accessGroup = accessGroup {
+ query[String(kSecAttrAccessGroup)] = accessGroup
+ }
+
+ var keyAttr = [String: Any]()
+ keyAttr[String(kSecAttrIsPermanent)] = true
+ keyAttr[String(kSecAttrApplicationTag)] = applicationTag
+
+#if !targetEnvironment(simulator)
+ // If the device supports Secure Enclave, create a keypair using Secure Enclave TokenID
+ if SecuredKey.isAvailable() {
+ query[String(kSecAttrTokenID)] = String(kSecAttrTokenIDSecureEnclave)
+ let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, .privateKeyUsage, nil)!
+ keyAttr[String(kSecAttrAccessControl)] = accessControl
+ }
+#endif
+
+ query[String(kSecPrivateKeyAttrs)] = keyAttr
+
+ var error: Unmanaged?
+ guard let privateKey = SecKeyCreateRandomKey(query as CFDictionary, &error) else {
+ throw error!.takeRetainedValue() as Error
+ }
+
+ return privateKey
+ }
+
+ /// Deletes private key with given 'Application Tag'
+ /// - Parameter applicationTag: Application Tag string value for private key
+ static func deleteKey(applicationTag: String) {
+ var query = [String: Any]()
+ query[String(kSecClass)] = String(kSecClassKey)
+ query[String(kSecAttrApplicationTag)] = applicationTag
+ SecItemDelete(query as CFDictionary)
+ }
+
+ /// Encrypts Data object using `SecuredKey` object
+ /// - Parameter data: Encrypted Data object
+ /// - Parameter secAlgorithm: Algorithm to be used for encryption. Default: `.eciesEncryptionCofactorVariableIVX963SHA256AESGCM`
+ /// - Returns: Encrypted Data object or `nil` if encryption fails
+ public func encrypt(data: Data, secAlgorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM) -> Data? {
+
+ guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, secAlgorithm) else {
+ return nil
+ }
+
+ var error: Unmanaged?
+ let encryptedData = SecKeyCreateEncryptedData(publicKey, secAlgorithm, data as CFData, &error) as Data?
+ return encryptedData
+ }
+
+ /// Decrypts Data object using SecuredKey object
+ /// - Parameter data: Decrypted Data object
+ /// - Parameter secAlgorithm: Algorithm to be used for decryption. Default: `.eciesEncryptionCofactorVariableIVX963SHA256AESGCM`
+ /// - Returns: Decrypted Data object or `nil` if decryption fails
+ public func decrypt(data: Data, secAlgorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM) -> Data? {
+
+ guard SecKeyIsAlgorithmSupported(privateKey, .decrypt, secAlgorithm) else {
+ return nil
+ }
+
+ var error: Unmanaged?
+ let decryptedData = SecKeyCreateDecryptedData(privateKey, secAlgorithm, data as CFData, &error) as Data?
+ if error != nil {
+ var decryptError: Unmanaged?
+ let decryptedData = SecKeyCreateDecryptedData(privateKey, oldAlgorithm, data as CFData, &decryptError) as Data?
+ if decryptError != nil {
+ return nil
+ } else {
+ return decryptedData
+ }
+
+ }
+ return decryptedData
+ }
+}
diff --git a/Storage/Storage/SecuredKeyEncryptor.swift b/Storage/Storage/SecuredKeyEncryptor.swift
new file mode 100644
index 0000000..800e5a0
--- /dev/null
+++ b/Storage/Storage/SecuredKeyEncryptor.swift
@@ -0,0 +1,68 @@
+//
+// SecuredKeyEncryptor.swift
+// PingStorage
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// A struct that provides encryption and decryption functionalities using `SecuredKey`.
+public struct SecuredKeyEncryptor: Encryptor {
+ private let securedKeyTag: String = "com.pingidentity.securedKey.identifier"
+ private var securedKey: SecuredKey
+
+ /// Initializes a new instance of `SecuredKeyEncryptor`.
+ ///
+ /// This initializer attempts to create a `SecuredKey` with the given application tag.
+ /// If it fails, the initializer returns `nil`.
+ public init?() {
+ guard let securedKey = SecuredKey(applicationTag: self.securedKeyTag) else {
+ return nil
+ }
+ self.securedKey = securedKey
+ }
+
+ /// Encrypts the given data.
+ /// - Parameter data: The data to encrypt.
+ /// - Returns: The encrypted data.
+ /// - Throws: `EncryptorError.failedToEncrypt` if the encryption fails.
+ public func encrypt(data: Data) async throws -> Data {
+ guard let encryptedData = securedKey.encrypt(data: data) else {
+ throw EncryptorError.failedToEncrypt
+ }
+ return encryptedData
+ }
+
+ /// Decrypts the given data.
+ /// - Parameter data: The data to decrypt.
+ /// - Returns: The decrypted data.
+ /// - Throws: `EncryptorError.failedToDecrypt` if the decryption fails.
+ public func decrypt(data: Data) async throws -> Data {
+ guard let decryptedData = securedKey.decrypt(data: data) else {
+ throw EncryptorError.failedToDecrypt
+ }
+ return decryptedData
+ }
+}
+
+
+/// `EncryptorError` represents errors that can occur while encrypting/decrypting.
+public enum EncryptorError: LocalizedError {
+ case failedToEncrypt
+ case failedToDecrypt
+
+ /// A localized message describing what error occurred.
+ public var errorMessage: String {
+ switch self {
+ case .failedToEncrypt:
+ return "Failed to encrypt given data"
+ case .failedToDecrypt:
+ return "Failed to decrypt given data"
+ }
+ }
+}
diff --git a/Storage/Storage/Storage.h b/Storage/Storage/Storage.h
new file mode 100644
index 0000000..212d12f
--- /dev/null
+++ b/Storage/Storage/Storage.h
@@ -0,0 +1,21 @@
+//
+// Storage.h
+// Storage
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+#import
+
+//! Project version number for Storage.
+FOUNDATION_EXPORT double StorageVersionNumber;
+
+//! Project version string for Storage.
+FOUNDATION_EXPORT const unsigned char StorageVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+
+
diff --git a/Storage/Storage/Storage.swift b/Storage/Storage/Storage.swift
new file mode 100644
index 0000000..ee9f1d0
--- /dev/null
+++ b/Storage/Storage/Storage.swift
@@ -0,0 +1,28 @@
+//
+// Storage.swift
+// PingStorage
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// Protocol to persist and retrieve `Codable` instanse.
+public protocol Storage {
+ associatedtype T: Codable
+
+ /// Saves the given item.
+ /// - Parameter item: The item to be saved.
+ func save(item: T) async throws
+
+ /// Retrieves the stored item.
+ /// - Returns: The stored item, or null if no item is stored.
+ func get() async throws -> T?
+
+ /// Deletes the stored item.
+ func delete() async throws
+}
diff --git a/Storage/Storage/StorageDelegate.swift b/Storage/Storage/StorageDelegate.swift
new file mode 100644
index 0000000..dee56d4
--- /dev/null
+++ b/Storage/Storage/StorageDelegate.swift
@@ -0,0 +1,94 @@
+//
+// StorageDelegate.swift
+// PingStorage
+//
+// Copyright (c) 2024 Ping Identity. 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
+
+/// A storage delegate class that delegates its operations to a storage.
+/// It can optionally cache the stored item in memory.
+/// This class is designed to be subclassed by specific storage strategies (e.g., keychain, in-memory) that conform to the `Storage` protocol.
+///
+/// - Parameter T: The type of the object being stored. Must conform to `Codable` to ensure that
+/// object can be easily encoded and decoded.
+open class StorageDelegate: Storage {
+ private let delegate: any Storage
+ private let cacheable: Bool
+ private var cached: T?
+ private let queue = DispatchQueue(label: "com.ping.storage.queue")
+
+ /// Initializer for StorageDelegate
+ /// - Parameters:
+ /// - delegate: The storage to delegate the operations to.
+ /// - cacheable: Whether the storage delegate should cache the object in memory.
+ public init(delegate: any Storage, cacheable: Bool = false) {
+ self.delegate = delegate
+ self.cacheable = cacheable
+ }
+
+ /// Saves the given item in the storage and optionally in memory.
+ /// - Parameter item: The item to save.
+ public func save(item: T) async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ queue.async {
+ Task {
+ do {
+ try await self.delegate.save(item: item)
+ if self.cacheable {
+ self.cached = item
+ }
+ continuation.resume()
+ } catch {
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+ }
+
+ /// Retrieves the item from memory if it's cached, otherwise from the storage.
+ /// - Returns: The item if it exists, `nil` otherwise.
+ public func get() async throws -> T? {
+ try await withCheckedThrowingContinuation { continuation in
+ queue.async {
+ Task {
+ do {
+ if let cached = self.cached {
+ continuation.resume(returning: cached)
+ } else {
+ let item = try await self.delegate.get()
+ continuation.resume(returning: item)
+ }
+ } catch {
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+ }
+
+ /// Deletes the item from the storage and removes it from memory if it's cached.
+ public func delete() async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ queue.async {
+ Task {
+ do {
+ try await self.delegate.delete()
+ if self.cacheable {
+ self.cached = nil
+ }
+ continuation.resume()
+ } catch {
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Storage/StorageTests/CustomEncryptorTests.swift b/Storage/StorageTests/CustomEncryptorTests.swift
new file mode 100644
index 0000000..8e80e58
--- /dev/null
+++ b/Storage/StorageTests/CustomEncryptorTests.swift
@@ -0,0 +1,70 @@
+//
+// CustomEncryptorTests.swift
+// StorageTests
+//
+// Copyright (c) 2024 Ping Identity. 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 XCTest
+@testable import PingStorage
+
+final class CustomEncryptorTests: XCTestCase {
+ var encryptor: CustomEncryptor!
+
+ override func setUp() {
+ super.setUp()
+ encryptor = CustomEncryptor()
+ }
+
+ override func tearDown() {
+ encryptor = nil
+ super.tearDown()
+ }
+
+ // TestRailCase(24716)
+ func testEncryption() async {
+ let data = Data("Test data".utf8)
+
+ do {
+ let encryptedData = try await encryptor.encrypt(data: data)
+ let encryptedString = String(decoding: encryptedData, as: UTF8.self)
+ XCTAssertTrue(encryptedString.contains("_ENCRYPTED_"), "Encrypted data should contain '_ENCRYPTED_'")
+ } catch {
+ XCTFail("Encryption failed with error: \(error)")
+ }
+ }
+
+ // TestRailCase(24716)
+ func testDecryption() async {
+ let data = Data("Test data".utf8)
+ let encryptedData = try! await encryptor.encrypt(data: data)
+
+ do {
+ let decryptedData = try await encryptor.decrypt(data: encryptedData)
+ let decryptedString = String(decoding: decryptedData, as: UTF8.self)
+ XCTAssertEqual(decryptedString, "Test data", "Decrypted data should match original data")
+ } catch {
+ XCTFail("Decryption failed with error: \(error)")
+ }
+ }
+}
+
+struct CustomEncryptor: Encryptor {
+
+ func encrypt(data: Data) async throws -> Data {
+ let stringData = String(decoding: data, as: UTF8.self)
+ let encryptedString = stringData + "_ENCRYPTED_"
+ return Data(encryptedString.utf8)
+ }
+
+ func decrypt(data: Data) async throws -> Data {
+ let stringData = String(decoding: data, as: UTF8.self)
+ let decryptedString = stringData.replacingOccurrences(of: "_ENCRYPTED_", with: "")
+ return Data(decryptedString.utf8)
+ }
+}
diff --git a/Storage/StorageTests/CustomStorageTests.swift b/Storage/StorageTests/CustomStorageTests.swift
new file mode 100644
index 0000000..6bf3cc8
--- /dev/null
+++ b/Storage/StorageTests/CustomStorageTests.swift
@@ -0,0 +1,77 @@
+//
+// CustomStorageTests.swift
+// StorageTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingStorage
+
+final class CustomStorageTests: XCTestCase {
+
+ private var customStorage: CustomStorageDelegate!
+
+ override func setUp() {
+ super.setUp()
+ customStorage = CustomStorageDelegate()
+ }
+
+ override func tearDown() {
+ customStorage = nil
+ super.tearDown()
+ }
+
+ // TestRailCase(24710)
+ func testSaveItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await customStorage.save(item: item)
+ let retrievedItem = try await customStorage.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ // TestRailCase(24711)
+ func testGetItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await customStorage.save(item: item)
+ let retrievedItem = try await customStorage.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ // TestRailCase(24714)
+ func testDeleteItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await customStorage.save(item: item)
+ try await customStorage.delete()
+ let retrievedItem = try await customStorage.get()
+ XCTAssertNil(retrievedItem)
+ }
+
+}
+
+public class CustomStorage: Storage {
+ private var data: T?
+
+ public func save(item: T) async throws {
+ data = item
+ }
+
+ public func get() async throws -> T? {
+ return data
+ }
+
+ public func delete() async throws {
+ data = nil
+ }
+
+}
+
+public class CustomStorageDelegate: StorageDelegate {
+ public init(cacheable: Bool = false) {
+ super.init(delegate: CustomStorage(), cacheable: cacheable)
+ }
+}
diff --git a/Storage/StorageTests/EncryptedKeychainStorageTests.swift b/Storage/StorageTests/EncryptedKeychainStorageTests.swift
new file mode 100644
index 0000000..6a22101
--- /dev/null
+++ b/Storage/StorageTests/EncryptedKeychainStorageTests.swift
@@ -0,0 +1,56 @@
+//
+// KeychainStorageTests.swift
+// StorageTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingStorage
+
+final class EncryptedKeychainStorageTests: XCTestCase {
+ private var keychainStorage: KeychainStorage!
+
+ override func setUp() {
+ super.setUp()
+ // Test KeychainStorage with the SecuredKeyEncryptor - the OOTB encryptor provided by the SDK
+ keychainStorage = KeychainStorage(account: "testAccount", encryptor: SecuredKeyEncryptor()!)
+ }
+
+ override func tearDown() {
+ Task {
+ try? await keychainStorage.delete()
+ keychainStorage = nil
+ }
+ super.tearDown()
+ }
+
+ // TestRailCase(24706)
+ func testSaveItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await keychainStorage.save(item: item)
+ let retrievedItem = try await keychainStorage.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ // TestRailCase(24707)
+ func testGetItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await keychainStorage.save(item: item)
+ let retrievedItem = try await keychainStorage.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ // TestRailCase(24708)
+ func testDeleteItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await keychainStorage.save(item: item)
+ try await keychainStorage.delete()
+ let retrievedItem = try await keychainStorage.get()
+ XCTAssertNil(retrievedItem)
+ }
+}
diff --git a/Storage/StorageTests/KeychainStorageTests.swift b/Storage/StorageTests/KeychainStorageTests.swift
new file mode 100644
index 0000000..867c632
--- /dev/null
+++ b/Storage/StorageTests/KeychainStorageTests.swift
@@ -0,0 +1,56 @@
+//
+// KeychainStorageTests.swift
+// StorageTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingStorage
+
+final class KeychainStorageTests: XCTestCase {
+ private var keychainStorage: KeychainStorage!
+
+ override func setUp() {
+ super.setUp()
+ // By default the KeychainStorage does not use encryption
+ keychainStorage = KeychainStorage(account: "testAccount")
+ }
+
+ override func tearDown() {
+ Task {
+ try? await keychainStorage.delete()
+ keychainStorage = nil
+ }
+ super.tearDown()
+ }
+
+ // TestRailCase(24703)
+ func testSaveItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await keychainStorage.save(item: item)
+ let retrievedItem = try await keychainStorage.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ // TestRailCase(24704)
+ func testGetItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await keychainStorage.save(item: item)
+ let retrievedItem = try await keychainStorage.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ // TestRailCase(24705)
+ func testDeleteItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await keychainStorage.save(item: item)
+ try await keychainStorage.delete()
+ let retrievedItem = try await keychainStorage.get()
+ XCTAssertNil(retrievedItem)
+ }
+}
diff --git a/Storage/StorageTests/MemoryStorageTests.swift b/Storage/StorageTests/MemoryStorageTests.swift
new file mode 100644
index 0000000..7558557
--- /dev/null
+++ b/Storage/StorageTests/MemoryStorageTests.swift
@@ -0,0 +1,80 @@
+//
+// MemoryStorageTests.swift
+// StorageTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingStorage
+
+final class MemoryStorageTests: XCTestCase {
+ private var memoryStorage: MemoryStorage!
+ private var memoryStorageMulti: MemoryStorage>!
+
+ override func setUp() {
+ super.setUp()
+ memoryStorage = MemoryStorage()
+ memoryStorageMulti = MemoryStorage()
+ }
+
+ override func tearDown() {
+ memoryStorage = nil
+ memoryStorageMulti = nil
+ super.tearDown()
+ }
+
+ // TestRailCase(21622)
+ func testSaveItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await memoryStorage.save(item: item)
+ let retrievedItem = try await memoryStorage.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ // TestRailCase(21623)
+ func testGetItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await memoryStorage.save(item: item)
+ let retrievedItem = try await memoryStorage.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ // TestRailCase(21626)
+ func testDeleteItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await memoryStorage.save(item: item)
+ try await memoryStorage.delete()
+ let retrievedItem = try await memoryStorage.get()
+ XCTAssertNil(retrievedItem)
+ }
+
+ // TestRailCase(21624, 21625)
+ func testMultipleData() async throws {
+ var itemsArray = [TestItem]()
+ let item1 = TestItem(id: 1, name: "Test1")
+ let item2 = TestItem(id: 2, name: "Test2")
+
+ itemsArray.append(item1)
+ itemsArray.append(item2)
+
+ // Save in memory storage
+ try await memoryStorageMulti.save(item: itemsArray)
+
+ // Restore from memory storage
+ let retrievedItem = try await memoryStorageMulti.get()
+
+ XCTAssertEqual(retrievedItem, itemsArray)
+ XCTAssertEqual(retrievedItem![0], item1)
+ XCTAssertEqual(retrievedItem![1], item2)
+ }
+}
+
+struct TestItem: Codable, Equatable {
+ let id: Int
+ let name: String
+}
diff --git a/Storage/StorageTests/SecuredKeyEncryptorTests.swift b/Storage/StorageTests/SecuredKeyEncryptorTests.swift
new file mode 100644
index 0000000..3780d39
--- /dev/null
+++ b/Storage/StorageTests/SecuredKeyEncryptorTests.swift
@@ -0,0 +1,59 @@
+//
+// SecuredKeyEncryptorTests.swift
+// StorageTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingStorage
+
+final class SecuredKeyEncryptorTests: XCTestCase {
+
+ var encryptor: SecuredKeyEncryptor?
+
+ override func setUp() {
+ super.setUp()
+ encryptor = SecuredKeyEncryptor()
+ }
+
+ override func tearDown() {
+ encryptor = nil
+ super.tearDown()
+ }
+
+ func testInitialization() {
+ XCTAssertNotNil(encryptor, "Initialization should succeed")
+ }
+
+ // TestRailCase(24709)
+ func testEncryption() async {
+ let data = Data("Test data".utf8)
+
+ do {
+ let encryptedData = try await encryptor?.encrypt(data: data)
+ XCTAssertNotNil(encryptedData, "Encryption should succeed")
+ } catch {
+ XCTFail("Encryption failed with error: \(error)")
+ }
+ }
+
+ // TestRailCase(24709)
+ func testDecryption() async {
+ let data = Data("Test data".utf8)
+
+ do {
+ let encryptedData = try await encryptor?.encrypt(data: data)
+ let decryptedData = try await encryptor?.decrypt(data: encryptedData!)
+
+ XCTAssertNotNil(decryptedData, "Decryption should succeed")
+ XCTAssertEqual(decryptedData, data, "Decrypted data should match original data")
+ } catch {
+ XCTFail("Decryption failed with error: \(error)")
+ }
+ }
+}
diff --git a/Storage/StorageTests/SecuredKeyTests.swift b/Storage/StorageTests/SecuredKeyTests.swift
new file mode 100644
index 0000000..a9135ec
--- /dev/null
+++ b/Storage/StorageTests/SecuredKeyTests.swift
@@ -0,0 +1,37 @@
+//
+// SecuredKeyTests.swift
+// StorageTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingStorage
+
+final class SecuredKeyTests: XCTestCase {
+
+ private var securedKey: SecuredKey!
+
+ override func setUp() {
+ super.setUp()
+ securedKey = SecuredKey(applicationTag: "com.pingidentity.securedKey.identifier")
+ }
+
+ override func tearDown() {
+ securedKey = nil
+ super.tearDown()
+ }
+
+ // TestRailCase(24709)
+ func testEncryptAndDecrypt() {
+ let data = "Test data".data(using: .utf8)!
+ let encryptedData = securedKey.encrypt(data: data)
+ XCTAssertNotNil(encryptedData)
+ let decryptedData = securedKey.decrypt(data: encryptedData!)
+ XCTAssertEqual(decryptedData, data)
+ }
+}
diff --git a/Storage/StorageTests/StorageDelegateTests.swift b/Storage/StorageTests/StorageDelegateTests.swift
new file mode 100644
index 0000000..36e8d63
--- /dev/null
+++ b/Storage/StorageTests/StorageDelegateTests.swift
@@ -0,0 +1,162 @@
+//
+// StorageDelegateTests.swift
+// StorageTests
+//
+// Copyright (c) 2024 Ping Identity. All rights reserved.
+//
+// This software may be modified and distributed under the terms
+// of the MIT license. See the LICENSE file for details.
+//
+
+
+import XCTest
+@testable import PingStorage
+
+final class StorageDelegateTests: XCTestCase {
+ private var storageDelegate: StorageDelegate!
+ private var memoryStorage: MemoryStorage!
+
+ override func setUp() {
+ super.setUp()
+ memoryStorage = MemoryStorage()
+ storageDelegate = StorageDelegate(delegate: memoryStorage, cacheable: false)
+ }
+
+ override func tearDown() {
+ storageDelegate = nil
+ memoryStorage = nil
+ super.tearDown()
+ }
+
+ func testSaveItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await storageDelegate.save(item: item)
+ let retrievedItem = try await storageDelegate.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ func testGetItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await storageDelegate.save(item: item)
+ let retrievedItem = try await storageDelegate.get()
+ XCTAssertEqual(retrievedItem, item)
+ }
+
+ func testDeleteItem() async throws {
+ let item = TestItem(id: 1, name: "Test")
+ try await storageDelegate.save(item: item)
+ try await storageDelegate.delete()
+ let retrievedItem = try await storageDelegate.get()
+ XCTAssertNil(retrievedItem)
+ }
+
+ func testConcurrentAccess() {
+ let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
+ let group = DispatchGroup()
+ let item = TestItem(id: 1, name: "Test")
+ let iterations = 1000
+
+ // Concurrent writes
+ for _ in 0..