diff --git a/COPYRIGHT b/COPYRIGHT index 7e20aac..3fff69d 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,4 +1,4 @@ -Copyright (c) 2023, Circle Technologies, LLC. All rights reserved. +Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy diff --git a/LICENSE b/LICENSE index bb442ab..085faa7 100644 --- a/LICENSE +++ b/LICENSE @@ -175,7 +175,7 @@ END OF TERMS AND CONDITIONS - © Circle Technologies, LLC 2023. All rights reserved. + © 2024, Circle Internet Financial, LTD. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 83ea33d..084096f 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,17 @@ $ brew ![image](readme_images/screenshot_4.png) - Set the `endPoint` in the `ContentView.swift` - Set the `appId` in the `ContentView.swift` + +5. (Optional) SSO configs setup + - If you want to use SSO for test , please change the flag `addSSOSignInView` value to *true* in the `w3s_ios_sample_app_walletsApp.swift` + ![image](readme_images/screenshot_5.png) + + Just set up the SSO you want to use below: + - [Apple] Update the App's Bundle Identifier to yours + ![image](readme_images/screenshot_6.png) + - [Google] Update `Info.plist` file to add your OAuth client ID and a custom URL scheme based on the reversed client ID. + Reference: [Get started with Google Sign-In for iOS](https://developers.google.com/identity/sign-in/ios/start-integrating#configure_app_project) + ![image](readme_images/screenshot_7.png) + - [Facebook] Replace the *APP-ID*, *CLIENT-TOKEN* and *APP-NAME* of `Info.plist` with your Facebook application configurations. + Reference: [Facebook Login for iOS - Quickstart](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) + ![image](readme_images/screenshot_8.png) diff --git a/Sample App/Podfile b/Sample App/Podfile index bb02486..847fc2f 100644 --- a/Sample App/Podfile +++ b/Sample App/Podfile @@ -1,4 +1,6 @@ -# Copyright (c) 2023, Circle Technologies, LLC. All rights reserved. +# Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,16 +20,26 @@ platform :ios, '13.0' target 'w3s-ios-sample-app-wallets' do # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! :linkage => :static + use_frameworks! + + # Applicable before CircleProgrammableWalletSDK version 1.0.12 + # use_frameworks! :linkage => :static pod 'CircleProgrammableWalletSDK' + # SSO SDK + pod 'GoogleSignIn' + pod 'FBSDKLoginKit' + end post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' + + # See this: https://developer.apple.com/forums/thread/725300 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' end end end diff --git a/Sample App/w3s-ios-sample-app-wallets.xcodeproj/project.pbxproj b/Sample App/w3s-ios-sample-app-wallets.xcodeproj/project.pbxproj index 43ac279..b2dd8ab 100644 --- a/Sample App/w3s-ios-sample-app-wallets.xcodeproj/project.pbxproj +++ b/Sample App/w3s-ios-sample-app-wallets.xcodeproj/project.pbxproj @@ -17,6 +17,12 @@ B3BEDEDF2A5064E800861533 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BEDEDD2A5064E800861533 /* ContentView.swift */; }; B3BEDEE62A50651700861533 /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BEDEE32A50651700861533 /* UserDefault.swift */; }; B3BEDEE82A50651700861533 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BEDEE52A50651700861533 /* Toast.swift */; }; + BA285C102B625BD600F317EF /* ToggleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA285C0F2B625BD600F317EF /* ToggleType.swift */; }; + BADB71432B4E7E43000AA7ED /* GoogleAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB71422B4E7E43000AA7ED /* GoogleAuthViewModel.swift */; }; + BADB71452B4E90AF000AA7ED /* SSOSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB71442B4E90AF000AA7ED /* SSOSignInView.swift */; }; + BADB71472B4FD11B000AA7ED /* AppleAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB71462B4FD11B000AA7ED /* AppleAuthViewModel.swift */; }; + BADB714A2B54DDF7000AA7ED /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB71492B54DDF7000AA7ED /* AppDelegate.swift */; }; + BADB714C2B54E33B000AA7ED /* FacebookAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB714B2B54E33B000AA7ED /* FacebookAuthViewModel.swift */; }; BAE182562AF36AD3004D6494 /* ChallengeResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE182552AF36AD3004D6494 /* ChallengeResultView.swift */; }; /* End PBXBuildFile section */ @@ -32,6 +38,14 @@ B3BEDEDD2A5064E800861533 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; B3BEDEE32A50651700861533 /* UserDefault.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = ""; }; B3BEDEE52A50651700861533 /* Toast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = ""; }; + BA285C0F2B625BD600F317EF /* ToggleType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleType.swift; sourceTree = ""; }; + BADB713D2B4E7567000AA7ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BADB71422B4E7E43000AA7ED /* GoogleAuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthViewModel.swift; sourceTree = ""; }; + BADB71442B4E90AF000AA7ED /* SSOSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOSignInView.swift; sourceTree = ""; }; + BADB71462B4FD11B000AA7ED /* AppleAuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAuthViewModel.swift; sourceTree = ""; }; + BADB71482B4FD2A4000AA7ED /* w3s-ios-sample-app-wallets.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "w3s-ios-sample-app-wallets.entitlements"; sourceTree = ""; }; + BADB71492B54DDF7000AA7ED /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BADB714B2B54E33B000AA7ED /* FacebookAuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthViewModel.swift; sourceTree = ""; }; BAE182552AF36AD3004D6494 /* ChallengeResultView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChallengeResultView.swift; sourceTree = ""; }; ED19DFB9C55BB7A8A6609DD9 /* Pods-w3s-ios-sample-app-wallets.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-w3s-ios-sample-app-wallets.release.xcconfig"; path = "Target Support Files/Pods-w3s-ios-sample-app-wallets/Pods-w3s-ios-sample-app-wallets.release.xcconfig"; sourceTree = ""; }; F095FAC15E5990E5728EEDA8 /* Pods_w3s_ios_sample_app_wallets.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_w3s_ios_sample_app_wallets.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -87,11 +101,15 @@ B3BEDECD2A50648300861533 /* w3s-ios-sample-app-wallets */ = { isa = PBXGroup; children = ( + BADB71482B4FD2A4000AA7ED /* w3s-ios-sample-app-wallets.entitlements */, + BADB71412B4E7E23000AA7ED /* ViewModels */, B3CD172C2A5DA09D000664EA /* Resources */, + BADB71492B54DDF7000AA7ED /* AppDelegate.swift */, B3BEDECE2A50648300861533 /* w3s_ios_sample_app_walletsApp.swift */, B3BEDEDD2A5064E800861533 /* ContentView.swift */, BAE182552AF36AD3004D6494 /* ChallengeResultView.swift */, B3BEDEDC2A5064E800861533 /* WalletSdkAdapter.swift */, + BADB71442B4E90AF000AA7ED /* SSOSignInView.swift */, B3BEDEE22A50651700861533 /* Helpers */, B3BEDED42A50648400861533 /* Preview Content */, ); @@ -111,6 +129,7 @@ children = ( B3BEDEE32A50651700861533 /* UserDefault.swift */, B3BEDEE52A50651700861533 /* Toast.swift */, + BA285C0F2B625BD600F317EF /* ToggleType.swift */, ); path = Helpers; sourceTree = ""; @@ -121,10 +140,21 @@ B3A66CE62A5B03F200FB0B2E /* Assets.xcassets */, B369BA8F2A8BF600007DEC09 /* CirclePWLocalizable.strings */, B369BA8E2A8BF600007DEC09 /* CirclePWTheme.json */, + BADB713D2B4E7567000AA7ED /* Info.plist */, ); path = Resources; sourceTree = ""; }; + BADB71412B4E7E23000AA7ED /* ViewModels */ = { + isa = PBXGroup; + children = ( + BADB71462B4FD11B000AA7ED /* AppleAuthViewModel.swift */, + BADB71422B4E7E43000AA7ED /* GoogleAuthViewModel.swift */, + BADB714B2B54E33B000AA7ED /* FacebookAuthViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -137,6 +167,7 @@ B3BEDEC82A50648300861533 /* Frameworks */, B3BEDEC92A50648300861533 /* Resources */, A6C197F8D8C91907C988C29B /* [CP] Copy Pods Resources */, + 402E39960CFD6974237CA94E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -195,6 +226,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 402E39960CFD6974237CA94E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-w3s-ios-sample-app-wallets/Pods-w3s-ios-sample-app-wallets-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-w3s-ios-sample-app-wallets/Pods-w3s-ios-sample-app-wallets-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-w3s-ios-sample-app-wallets/Pods-w3s-ios-sample-app-wallets-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; A6C197F8D8C91907C988C29B /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -241,12 +289,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BADB71472B4FD11B000AA7ED /* AppleAuthViewModel.swift in Sources */, + BADB714C2B54E33B000AA7ED /* FacebookAuthViewModel.swift in Sources */, B3BEDEE82A50651700861533 /* Toast.swift in Sources */, B3BEDEE62A50651700861533 /* UserDefault.swift in Sources */, B3BEDECF2A50648300861533 /* w3s_ios_sample_app_walletsApp.swift in Sources */, B3BEDEDE2A5064E800861533 /* WalletSdkAdapter.swift in Sources */, + BADB71432B4E7E43000AA7ED /* GoogleAuthViewModel.swift in Sources */, B3BEDEDF2A5064E800861533 /* ContentView.swift in Sources */, + BADB71452B4E90AF000AA7ED /* SSOSignInView.swift in Sources */, + BADB714A2B54DDF7000AA7ED /* AppDelegate.swift in Sources */, BAE182562AF36AD3004D6494 /* ChallengeResultView.swift in Sources */, + BA285C102B625BD600F317EF /* ToggleType.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -373,12 +427,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "w3s-ios-sample-app-wallets/w3s-ios-sample-app-wallets.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"w3s-ios-sample-app-wallets/Preview Content\""; DEVELOPMENT_TEAM = 6R3336A52L; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "w3s-ios-sample-app-wallets/Resources/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "w3s-ios-sample"; INFOPLIST_KEY_NSFaceIDUsageDescription = "Enable Biometrics PIN"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -406,12 +462,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "w3s-ios-sample-app-wallets/w3s-ios-sample-app-wallets.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"w3s-ios-sample-app-wallets/Preview Content\""; DEVELOPMENT_TEAM = 6R3336A52L; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "w3s-ios-sample-app-wallets/Resources/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "w3s-ios-sample"; INFOPLIST_KEY_NSFaceIDUsageDescription = "Enable Biometrics PIN"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; diff --git a/Sample App/w3s-ios-sample-app-wallets/AppDelegate.swift b/Sample App/w3s-ios-sample-app-wallets/AppDelegate.swift new file mode 100644 index 0000000..183a30d --- /dev/null +++ b/Sample App/w3s-ios-sample-app-wallets/AppDelegate.swift @@ -0,0 +1,45 @@ +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI +import GoogleSignIn +import FBSDKLoginKit + +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + return addSSOSignInView ? + ApplicationDelegate.shared.application(application, didFinishLaunchingWithOptions: launchOptions) : true + } + + func application(_ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + guard addSSOSignInView else { + return false + } + + if GIDSignIn.sharedInstance.handle(url) { + return true + } + + if ApplicationDelegate.shared.application(app, open: url, options: options) { + return true + } + + return false + } +} diff --git a/Sample App/w3s-ios-sample-app-wallets/ChallengeResultView.swift b/Sample App/w3s-ios-sample-app-wallets/ChallengeResultView.swift index 4f4af96..0a7c5ba 100644 --- a/Sample App/w3s-ios-sample-app-wallets/ChallengeResultView.swift +++ b/Sample App/w3s-ios-sample-app-wallets/ChallengeResultView.swift @@ -1,9 +1,18 @@ +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. // -// ChallengeResultView.swift -// w3s-ios-sample-app-wallets +// SPDX-License-Identifier: Apache-2.0 // -// Created by CIRCLE on 2023/11/1. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. import SwiftUI import CircleProgrammableWalletSDK diff --git a/Sample App/w3s-ios-sample-app-wallets/ContentView.swift b/Sample App/w3s-ios-sample-app-wallets/ContentView.swift index c4e8728..9559526 100644 --- a/Sample App/w3s-ios-sample-app-wallets/ContentView.swift +++ b/Sample App/w3s-ios-sample-app-wallets/ContentView.swift @@ -1,4 +1,6 @@ -// Copyright (c) 2023, Circle Technologies, LLC. All rights reserved. +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,13 +27,16 @@ struct ContentView: View { @State var userToken = "" @State var encryptionKey = "" @State var challengeId = "" + @State var pinCodeDeviceShare = "" @State var enableBiometrics = false + @State var disableConfirmationUI = false @State var showToast = false @State var toastMessage: String? @State var toastConfig: Toast.Config = .init() @State var path = NavigationPath() + @State var deviceID: String = "" var body: some View { NavigationStack(path: $path) { @@ -43,7 +48,8 @@ struct ContentView: View { sectionInputField("User Token", binding: $userToken) sectionInputField("Encryption Key", binding: $encryptionKey) sectionInputField("Challenge ID", binding: $challengeId) - sectionToggle("Biometrics", binding: $enableBiometrics) + sectionInputField("PinCode Device Share", binding: $pinCodeDeviceShare) + sectionToggles sectionButtons // TestButtons @@ -54,6 +60,17 @@ struct ContentView: View { .navigationDestination(for: CircleProgrammableWalletSDK.ExecuteCompletionStruct.self) { executeResult in ChallengeResultView(executeResult: executeResult, path: $path) } + .toolbar { + if addSSOSignInView { + ToolbarItem { + NavigationLink { + SSOSignInView(deviceID: $deviceID) + } label: { + Text("SSO") + } + } + } + } } .scrollContentBackground(.hidden) .onAppear { @@ -62,6 +79,8 @@ struct ContentView: View { if let storedAppId = self.adapter.storedAppId, !storedAppId.isEmpty { self.appId = storedAppId } + + deviceID = WalletSdk.shared.getDeviceId() } .onChange(of: appId) { newValue in if let errStr = self.adapter.updateEndPoint(endPoint, appId: newValue, biometrics: enableBiometrics) { @@ -70,7 +89,12 @@ struct ContentView: View { self.adapter.storedAppId = newValue } .onChange(of: enableBiometrics) { newValue in - if let errStr = self.adapter.updateEndPoint(endPoint, appId: appId, biometrics: newValue) { + if let errStr = self.adapter.updateEndPoint(endPoint, appId: appId, biometrics: newValue, disableUI: disableConfirmationUI) { + showToast(.failure, message: "Error: " + errStr) + } + } + .onChange(of: disableConfirmationUI) { newValue in + if let errStr = self.adapter.updateEndPoint(endPoint, appId: appId, biometrics: enableBiometrics, disableUI: newValue) { showToast(.failure, message: "Error: " + errStr) } } @@ -104,15 +128,25 @@ struct ContentView: View { } } - func sectionToggle(_ title: String, binding: Binding) -> some View { + var sectionToggles: some View { Section { - Toggle(isOn: binding) { - HStack { - Text(title) - Image(systemName: "faceid") - } + addToggle(ToggleType.biometrics, binding: $enableBiometrics) + addToggle(ToggleType.confirmUI, binding: $disableConfirmationUI) + } + .listRowSeparator(.hidden) + .listRowInsets(.none) + .padding(.all, .zero) + } + + func addToggle(_ type: ToggleType, binding: Binding) -> some View { + Toggle(isOn: binding) { + HStack { + Text(type.desc) + type.icon } } + .listRowInsets(.none) + .padding(.all, .zero) } var sectionButtons: some View { @@ -128,7 +162,7 @@ struct ContentView: View { guard !userToken.isEmpty else { showToast(.general, message: "User Token is Empty"); return } guard !encryptionKey.isEmpty else { showToast(.general, message: "Encryption Key is Empty"); return } guard !challengeId.isEmpty else { showToast(.general, message: "Challenge ID is Empty"); return } - executeChallenge(userToken: userToken, encryptionKey: encryptionKey, challengeId: challengeId) + executeChallenge(userToken: userToken, encryptionKey: encryptionKey, challengeId: challengeId, pinCodeDeviceShare: pinCodeDeviceShare) } label: { Text("Execute") @@ -182,39 +216,20 @@ extension ContentView { toastConfig = Toast.Config(backgroundColor: .pink, duration: 10.0) } } - - func executeChallenge(userToken: String, encryptionKey: String, challengeId: String) { - var showChallengeResult = true - - WalletSdk.shared.execute(userToken: userToken, - encryptionKey: encryptionKey, - challengeIds: [challengeId]) { response in - switch response.result { - case .success(let result): - let challengeStatus = result.status.rawValue - let challeangeType = result.resultType.rawValue - let warningType = response.onWarning?.warningType - let warningString = warningType != nil ? - " (\(warningType!))" : "" - showToast(.success, message: "\(challeangeType) - \(challengeStatus)\(warningString)") - - response.onErrorController?.dismiss(animated: true) - - case .failure(let error): - showToast(.failure, message: "Error: " + error.displayString) - errorHandler(apiError: error, onErrorController: response.onErrorController) - - if error.errorCode == .userCanceled { - showChallengeResult = false - } + + func executeChallenge(userToken: String, encryptionKey: String, challengeId: String, pinCodeDeviceShare: String) { + if !pinCodeDeviceShare.isEmpty { + WalletSdk.shared.executeWithUserSecret(userToken: userToken, + encryptionKey: encryptionKey, + userSecret: pinCodeDeviceShare, + challengeIds: [challengeId]) { response in + executeResponseHandler(response) } - - if let onWarning = response.onWarning { - print(onWarning) - } - - if showChallengeResult { - path.append(response) + } else { + WalletSdk.shared.execute(userToken: userToken, + encryptionKey: encryptionKey, + challengeIds: [challengeId]) { response in + executeResponseHandler(response) } } } @@ -238,6 +253,38 @@ extension ContentView { } } + func executeResponseHandler(_ response: ExecuteCompletionStruct) { + var showChallengeResult = true + + switch response.result { + case .success(let result): + let challengeStatus = result.status.rawValue + let challeangeType = result.resultType.rawValue + let warningType = response.onWarning?.warningType + let warningString = warningType != nil ? + " (\(warningType!))" : "" + showToast(.success, message: "\(challeangeType) - \(challengeStatus)\(warningString)") + + response.onErrorController?.dismiss(animated: true) + + case .failure(let error): + showToast(.failure, message: "Error: " + error.displayString) + errorHandler(apiError: error, onErrorController: response.onErrorController) + + if error.errorCode == .userCanceled { + showChallengeResult = false + } + } + + if let onWarning = response.onWarning { + print(onWarning) + } + + if showChallengeResult { + path.append(response) + } + } + func errorHandler(apiError: ApiError, onErrorController: UINavigationController?) { switch apiError.errorCode { case .userHasSetPin, diff --git a/Sample App/w3s-ios-sample-app-wallets/Helpers/Toast.swift b/Sample App/w3s-ios-sample-app-wallets/Helpers/Toast.swift index ce7b721..34f934a 100644 --- a/Sample App/w3s-ios-sample-app-wallets/Helpers/Toast.swift +++ b/Sample App/w3s-ios-sample-app-wallets/Helpers/Toast.swift @@ -1,4 +1,6 @@ -// Copyright (c) 2023, Circle Technologies, LLC. All rights reserved. +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sample App/w3s-ios-sample-app-wallets/Helpers/ToggleType.swift b/Sample App/w3s-ios-sample-app-wallets/Helpers/ToggleType.swift new file mode 100644 index 0000000..7817ae0 --- /dev/null +++ b/Sample App/w3s-ios-sample-app-wallets/Helpers/ToggleType.swift @@ -0,0 +1,40 @@ +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI + +enum ToggleType { + case biometrics + case confirmUI + + var desc: String { + switch self { + case .biometrics: + return "Biometrics" + case .confirmUI: + return "Disable Confirmation UI" + } + } + + var icon: Image { + switch self { + case .biometrics: + return Image(systemName: "faceid") + case .confirmUI: + return Image(systemName: "person.circle") + } + } +} diff --git a/Sample App/w3s-ios-sample-app-wallets/Helpers/UserDefault.swift b/Sample App/w3s-ios-sample-app-wallets/Helpers/UserDefault.swift index 65e777f..ade0098 100644 --- a/Sample App/w3s-ios-sample-app-wallets/Helpers/UserDefault.swift +++ b/Sample App/w3s-ios-sample-app-wallets/Helpers/UserDefault.swift @@ -1,4 +1,6 @@ -// Copyright (c) 2023, Circle Technologies, LLC. All rights reserved. +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sample App/w3s-ios-sample-app-wallets/Resources/CirclePWTheme.json b/Sample App/w3s-ios-sample-app-wallets/Resources/CirclePWTheme.json index f78b2b8..0929c46 100644 --- a/Sample App/w3s-ios-sample-app-wallets/Resources/CirclePWTheme.json +++ b/Sample App/w3s-ios-sample-app-wallets/Resources/CirclePWTheme.json @@ -34,6 +34,7 @@ "pin_dot_base_border": "#707070", "pin_dot_activated": "#0073C3", "pin_dot_focused": "#0073C3", + "pin_digit_activated": "#ff6699", "input_border": "#E8E8E8", "input_border_focused": "#46B5FF", diff --git a/Sample App/w3s-ios-sample-app-wallets/Resources/Info.plist b/Sample App/w3s-ios-sample-app-wallets/Resources/Info.plist new file mode 100644 index 0000000..1b454d8 --- /dev/null +++ b/Sample App/w3s-ios-sample-app-wallets/Resources/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleURLTypes + + + CFBundleURLSchemes + + Your dot reversed iOS Client ID + fbAPP-ID + + + + GIDClientID + Your OAuth iOS client ID + FacebookAppID + APP-ID + FacebookClientToken + CLIENT-TOKEN + FacebookDisplayName + APP-NAME + + diff --git a/Sample App/w3s-ios-sample-app-wallets/SSOSignInView.swift b/Sample App/w3s-ios-sample-app-wallets/SSOSignInView.swift new file mode 100644 index 0000000..c27b4ce --- /dev/null +++ b/Sample App/w3s-ios-sample-app-wallets/SSOSignInView.swift @@ -0,0 +1,171 @@ +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI +import CircleProgrammableWalletSDK +import FBSDKLoginKit + +struct SSOSignInView: View { + + @Environment(\.colorScheme) var colorScheme + @Environment(\.dismiss) var dismiss + + @EnvironmentObject var appleViewModel: AppleAuthViewModel + @EnvironmentObject var googleViewModel: GoogleAuthViewModel + @EnvironmentObject var facebookViewModel: FacebookAuthViewModel + + @Binding var deviceID: String + @State var providerName = "{Provider Name}" + @State var ssoToken = "SSO Token" + + @State var showToast = false + @State var toastMessage: String? + @State var toastConfig: Toast.Config = .init() + + var body: some View { + VStack { + List { + sectionInputField("Device ID", text: deviceID) + appleViewModel.SignInButton() + .listRowSeparator(.hidden) + Button(action: googleViewModel.signInOutHandler) { + let isGoogleSign = googleViewModel.state == .signedIn + let buttonTitle = isGoogleSign ? "Sign out Google" : "Sign in with Google" + + Text(buttonTitle) + .font(.system(size: 23)) + .padding([.top, .bottom], 18) + .frame(maxWidth: .infinity, alignment: .center) + .fontWeight(.medium) + .foregroundColor(.white) + .background( + RoundedRectangle( + cornerRadius: 5, + style: .continuous + ).fill(Color(red: 61 / 255, green: 54 / 255, blue: 82 / 255)) + ) + } + .listRowSeparator(.hidden) + Button(action: facebookViewModel.signInOutHandler) { + let isGoogleSign = facebookViewModel.state == .signedIn + let buttonTitle = isGoogleSign ? "Sign out Facebook" : "Sign in with Facebook" + + Text(buttonTitle) + .font(.system(size: 23)) + .padding([.top, .bottom], 18) + .frame(maxWidth: .infinity, alignment: .center) + .fontWeight(.medium) + .foregroundColor(.white) + .background( + RoundedRectangle( + cornerRadius: 5, + style: .continuous + ).fill(Color(red: 24 / 255, green: 119 / 255, blue: 242 / 255)) + ) + } + sectionInputField("SSO Token", binding: $ssoToken) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onChange(of: appleViewModel.token) { newValue in + providerName = "Apple" + ssoToken = newValue + } + .onChange(of: googleViewModel.state) { newValue in + providerName = "Google" + switch newValue { + case .signedIn: + ssoToken = googleViewModel.token + case .signedOut: + ssoToken = "SSO Token" + } + } + .onChange(of: facebookViewModel.state) { newValue in + providerName = "Facebook" + switch newValue { + case .signedIn: + ssoToken = facebookViewModel.token + case .signedOut: + ssoToken = "SSO Token" + } + } + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + dismiss() + } label: { + Image(uiImage: UIImage(named: "ic_navi_back")!) + } + } + } + .toast(message: toastMessage ?? "", + isShowing: $showToast, + config: toastConfig) + } + + func sectionInputField(_ title: String, text: String) -> Section { + Section { + Button(action: { + UIPasteboard.general.string = text + showToast(message: "Copied: \(text)") + }) { + Text(text) + .foregroundColor(.black) + .frame(maxWidth: .infinity, alignment: .leading) + .multilineTextAlignment(.leading) + } + .buttonStyle(.bordered) + .buttonBorderShape(.roundedRectangle) + } header: { + Text(title + " :") + } + } + + func sectionInputField(_ title: String, binding: Binding) -> Section { + Section { + Button(action: { + UIPasteboard.general.string = binding.wrappedValue + showToast(message: "SSO token copied!") + }) { + Text(binding.wrappedValue) + .foregroundColor(colorScheme == .light ? Color.black : Color.white) + .frame(maxWidth: .infinity, alignment: .leading) + .multilineTextAlignment(.leading) + .lineLimit(nil) + .truncationMode(.tail) + } + } header: { + var content: AttributedString { + var content = AttributedString("SSO (\(providerName)) Token" + " :") + content.inlinePresentationIntent = .stronglyEmphasized + return content + } + Text(content) + } + } +} + +extension SSOSignInView { + + func showToast(message: String) { + toastMessage = message + showToast = true + + toastConfig = Toast.Config() + } + +} diff --git a/Sample App/w3s-ios-sample-app-wallets/ViewModels/AppleAuthViewModel.swift b/Sample App/w3s-ios-sample-app-wallets/ViewModels/AppleAuthViewModel.swift new file mode 100644 index 0000000..51e5457 --- /dev/null +++ b/Sample App/w3s-ios-sample-app-wallets/ViewModels/AppleAuthViewModel.swift @@ -0,0 +1,71 @@ +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI +import AuthenticationServices + +class AppleAuthViewModel: ObservableObject { + + enum SignInState { + case signedIn + case signedOut + } + + @Published var token: String = "" + + var state: SignInState = .signedOut + + func SignInButton(_ type: SignInWithAppleButton.Style = .black) -> some View { + return SignInWithAppleButton(.signIn) { request in + request.requestedScopes = [.fullName, .email] + } onCompletion: { result in + switch result { + case .success(let authResults): + switch authResults.credential { + case let appleIDCredential as ASAuthorizationAppleIDCredential: + self.state = .signedIn + print("Apple signed in successfully!") + + if let identityToken = appleIDCredential.identityToken { + let jwtToken = String(decoding: identityToken, as: UTF8.self) + self.token = jwtToken + print("SSO token:\n\(jwtToken)") + } + case let singleSignOnCredential as ASAuthorizationSingleSignOnCredential: + self.state = .signedIn + print("Single signed in successfully!") + + if let identityToken = singleSignOnCredential.identityToken { + let jwtToken = String(decoding: identityToken, as: UTF8.self) + self.token = jwtToken + print("SSO token:\n\(jwtToken)") + } + case _ as ASPasswordCredential: + // Sign in using an existing iCloud Keychain credential. + print("iCloud Keychain signed in successfully, maybe the JWT token need to get from backend service!") + default: + break + } + + case .failure(let error): + print("Apple authorisation failed: \(error.localizedDescription)") + } + } + .frame(width: nil, height: 60, alignment: .center) + .signInWithAppleButtonStyle(type) + } + +} diff --git a/Sample App/w3s-ios-sample-app-wallets/ViewModels/FacebookAuthViewModel.swift b/Sample App/w3s-ios-sample-app-wallets/ViewModels/FacebookAuthViewModel.swift new file mode 100644 index 0000000..9c228be --- /dev/null +++ b/Sample App/w3s-ios-sample-app-wallets/ViewModels/FacebookAuthViewModel.swift @@ -0,0 +1,62 @@ +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FBSDKLoginKit + +class FacebookAuthViewModel: ObservableObject { + + enum SignInState { + case signedIn + case signedOut + } + + let loginManager = LoginManager() + + @Published var state: SignInState = .signedOut + + var token: String = "" + + func signInOutHandler() { + state == .signedIn ? signOut() : signIn() + } + + func signIn() { + loginManager.logIn(permissions: ["public_profile", "email"], from: nil) { loginResult, error in + if let loginResult { + if loginResult.isCancelled { + print("User cancel sign in with Facebook") + } else { + self.state = .signedIn + print("Facebook signed in successfully!") + + if let jwtToken = loginResult.token?.tokenString { + self.token = jwtToken + } + } + } else if let error { + print("Facebook authorisation failed: \(error.localizedDescription)") + } + } + } + + func signOut() { + loginManager.logOut() + + state = .signedOut + print("Already Facebook signed out!") + } + +} diff --git a/Sample App/w3s-ios-sample-app-wallets/ViewModels/GoogleAuthViewModel.swift b/Sample App/w3s-ios-sample-app-wallets/ViewModels/GoogleAuthViewModel.swift new file mode 100644 index 0000000..9d5822d --- /dev/null +++ b/Sample App/w3s-ios-sample-app-wallets/ViewModels/GoogleAuthViewModel.swift @@ -0,0 +1,82 @@ +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI +import GoogleSignIn + +class GoogleAuthViewModel: ObservableObject { + + enum SignInState { + case signedIn + case signedOut + } + + @Published var state: SignInState = .signedOut + + var token: String = "" + + func signInOutHandler() { + state == .signedIn ? signOut() : signIn() + } + + func signIn() { + if GIDSignIn.sharedInstance.hasPreviousSignIn() { + GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in + if error != nil || user == nil { + self.signOut() + + if let error { + print(error.localizedDescription) + } + } else { + self.state = .signedIn + print("Already signed in with Google!") + + if let token = user?.idToken?.tokenString { + self.token = token + print("SSO token:\n\(token)") + } + } + } + } else { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } + + guard let rootViewController = windowScene.windows.first?.rootViewController else { return } + + GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController, completion: { signResult, error in + if let error { + print(error.localizedDescription) + } else { + self.state = .signedIn + print("Google signed in successfully!") + + if let token = signResult?.user.idToken?.tokenString { + self.token = token + print("SSO token:\n\(token)") + } + } + }) + } + } + + func signOut() { + GIDSignIn.sharedInstance.signOut() + + state = .signedOut + print("Already Google signed out!") + } + +} diff --git a/Sample App/w3s-ios-sample-app-wallets/WalletSdkAdapter.swift b/Sample App/w3s-ios-sample-app-wallets/WalletSdkAdapter.swift index 44eacc8..debc7a1 100644 --- a/Sample App/w3s-ios-sample-app-wallets/WalletSdkAdapter.swift +++ b/Sample App/w3s-ios-sample-app-wallets/WalletSdkAdapter.swift @@ -1,4 +1,6 @@ -// Copyright (c) 2023, Circle Technologies, LLC. All rights reserved. +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,13 +28,13 @@ class WalletSdkAdapter { WalletSdk.shared.setLayoutProvider(self) WalletSdk.shared.setErrorMessenger(self) WalletSdk.shared.setDelegate(self) - WalletSdk.shared.customUserAgent = "IOS-SAMPLE-APP-WALLETS" } @discardableResult - func updateEndPoint(_ endPoint: String, appId: String, biometrics: Bool = false) -> String? { + func updateEndPoint(_ endPoint: String, appId: String, + biometrics: Bool = false, disableUI: Bool = false) -> String? { let _appId = appId.trimmingCharacters(in: .whitespacesAndNewlines) - let settings: WalletSdk.SettingsManagement = .init(enableBiometricsPin: biometrics) + let settings: WalletSdk.SettingsManagement = .init(enableBiometricsPin: biometrics, disableConfirmationUI: disableUI) let configuration = WalletSdk.Configuration(endPoint: endPoint, appId: _appId, settingsManagement: settings) do { diff --git a/Sample App/w3s-ios-sample-app-wallets/w3s-ios-sample-app-wallets.entitlements b/Sample App/w3s-ios-sample-app-wallets/w3s-ios-sample-app-wallets.entitlements new file mode 100644 index 0000000..a812db5 --- /dev/null +++ b/Sample App/w3s-ios-sample-app-wallets/w3s-ios-sample-app-wallets.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + + diff --git a/Sample App/w3s-ios-sample-app-wallets/w3s_ios_sample_app_walletsApp.swift b/Sample App/w3s-ios-sample-app-wallets/w3s_ios_sample_app_walletsApp.swift index 236bcff..9c3b25a 100644 --- a/Sample App/w3s-ios-sample-app-wallets/w3s_ios_sample_app_walletsApp.swift +++ b/Sample App/w3s-ios-sample-app-wallets/w3s_ios_sample_app_walletsApp.swift @@ -1,4 +1,6 @@ -// Copyright (c) 2023, Circle Technologies, LLC. All rights reserved. +// Copyright (c) 2024, Circle Internet Financial, LTD. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,11 +16,19 @@ import SwiftUI +let addSSOSignInView = false // Add SSO sign in view for test + @main struct w3s_ios_sample_app_walletsApp: App { + + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + var body: some Scene { WindowGroup { ContentView() + .environmentObject(AppleAuthViewModel()) + .environmentObject(GoogleAuthViewModel()) + .environmentObject(FacebookAuthViewModel()) } } } diff --git a/readme_images/screenshot_5.png b/readme_images/screenshot_5.png new file mode 100644 index 0000000..5060652 Binary files /dev/null and b/readme_images/screenshot_5.png differ diff --git a/readme_images/screenshot_6.png b/readme_images/screenshot_6.png new file mode 100644 index 0000000..cc67784 Binary files /dev/null and b/readme_images/screenshot_6.png differ diff --git a/readme_images/screenshot_7.png b/readme_images/screenshot_7.png new file mode 100644 index 0000000..3cc0287 Binary files /dev/null and b/readme_images/screenshot_7.png differ diff --git a/readme_images/screenshot_8.png b/readme_images/screenshot_8.png new file mode 100644 index 0000000..2f4cbb8 Binary files /dev/null and b/readme_images/screenshot_8.png differ