From 59ef2f5bc33ee99a6830ab8e4ad77b9226fb35fd Mon Sep 17 00:00:00 2001 From: brandtkruger Date: Wed, 8 Oct 2025 17:46:53 +0200 Subject: [PATCH 1/8] Entitlements and Hard Check --- .build/checkouts/AppAuth-iOS | 1 + KindeSDK.xcodeproj/project.pbxproj | 47 +- KindeSDK.xcodeproj/project.pbxproj.backup | 832 ++++++++++++++++++++++ Sources/KindeSDK/Auth/Auth.swift | 591 ++++++++++++++- Sources/KindeSDK/Auth/AuthError.swift | 28 + Tests/AuthSpec.swift | 30 +- Tests/EntitlementsSpec.swift | 366 ++++++++++ 7 files changed, 1861 insertions(+), 34 deletions(-) create mode 160000 .build/checkouts/AppAuth-iOS create mode 100644 KindeSDK.xcodeproj/project.pbxproj.backup create mode 100644 Tests/EntitlementsSpec.swift diff --git a/.build/checkouts/AppAuth-iOS b/.build/checkouts/AppAuth-iOS new file mode 160000 index 0000000..71cde44 --- /dev/null +++ b/.build/checkouts/AppAuth-iOS @@ -0,0 +1 @@ +Subproject commit 71cde449f13d453227e687458144bde372d30fc7 diff --git a/KindeSDK.xcodeproj/project.pbxproj b/KindeSDK.xcodeproj/project.pbxproj index a366f07..a6f4a7d 100644 --- a/KindeSDK.xcodeproj/project.pbxproj +++ b/KindeSDK.xcodeproj/project.pbxproj @@ -24,8 +24,14 @@ 3B41B1DF2A0E5F9100EFD9E4 /* AuthStateRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C12A0E5F9100EFD9E4 /* AuthStateRepository.swift */; }; 3B41B1E02A0E5F9100EFD9E4 /* AuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C22A0E5F9100EFD9E4 /* AuthError.swift */; }; 3B41B1E12A0E5F9100EFD9E4 /* DefaultLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C32A0E5F9100EFD9E4 /* DefaultLogger.swift */; }; - 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */; }; - 3B41B1E32A0E5F9100EFD9E4 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */; }; + 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */; + 36821D0699A84EDFBD69C26D /* Entitlement.swift */; + 3B80D15350C34728A7DE72DA /* FeatureFlag.swift */; + CFBB2F51F97B4F0FA1B6C317 /* Claims.swift */; }; + 3B41B1E32A0E5F9100EFD9E4 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */; + C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift */; + 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift */; + 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift */; }; 3B41B1E42A0E5F9100EFD9E4 /* BearerTokenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C62A0E5F9100EFD9E4 /* BearerTokenHandler.swift */; }; 3B41B1E52A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C72A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift */; }; 3B41B1E62A0E5F9100EFD9E4 /* BearerRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C82A0E5F9100EFD9E4 /* BearerRequestBuilder.swift */; }; @@ -55,6 +61,20 @@ D773EAD42A897ED30049DDEE /* FlagError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773EAD32A897ED30049DDEE /* FlagError.swift */; }; D7FD1F682A8D59E20088802F /* SDKVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD1F672A8D59E20088802F /* SDKVersion.swift */; }; D7FD1F6E2A8D5D390088802F /* PList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD1F6D2A8D5D390088802F /* PList.swift */; }; + 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */; + 36821D0699A84EDFBD69C26D /* Entitlement.swift */; + 3B80D15350C34728A7DE72DA /* FeatureFlag.swift */; + CFBB2F51F97B4F0FA1B6C317 /* Claims.swift */; }; + 3B41B1E32A0E5F9100EFD9E4 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */; + C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift */; + 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift */; + 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift */; }; + 36821D0699A84EDFBD69C26D /* Entitlement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36821D0699A84EDFBD69C26D /* Entitlement.swift */; }; + 3B80D15350C34728A7DE72DA /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D15350C34728A7DE72DA /* FeatureFlag.swift */; }; + CFBB2F51F97B4F0FA1B6C317 /* Claims.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBB2F51F97B4F0FA1B6C317 /* Claims.swift */; }; + C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift */; }; + 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift */; }; + 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -87,7 +107,12 @@ 3B41B1C22A0E5F9100EFD9E4 /* AuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthError.swift; sourceTree = ""; }; 3B41B1C32A0E5F9100EFD9E4 /* DefaultLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLogger.swift; sourceTree = ""; }; 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = ""; }; - 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 36821D0799A84EDFBD69C26D /* FeatureFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; + 36821D0899A84EDFBD69C26D /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; + 36821D0999A84EDFBD69C26D /* EntitlementsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntitlementsService.swift; sourceTree = ""; }; + 36821D0A99A84EDFBD69C26D /* FeatureFlagsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlagsService.swift; sourceTree = ""; }; + 36821D0B99A84EDFBD69C26D /* ClaimsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClaimsService.swift; sourceTree = ""; }; 3B41B1C62A0E5F9100EFD9E4 /* BearerTokenHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerTokenHandler.swift; sourceTree = ""; }; 3B41B1C72A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerRequestBuilderFactory.swift; sourceTree = ""; }; 3B41B1C82A0E5F9100EFD9E4 /* BearerRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerRequestBuilder.swift; sourceTree = ""; }; @@ -111,6 +136,19 @@ D7FD1F672A8D59E20088802F /* SDKVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKVersion.swift; sourceTree = ""; }; D7FD1F6C2A8D5CEC0088802F /* KindeSDK-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "KindeSDK-Info.plist"; sourceTree = ""; }; D7FD1F6D2A8D5D390088802F /* PList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PList.swift; sourceTree = ""; }; + 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = ""; }; + 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 36821D0799A84EDFBD69C26D /* FeatureFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; + 36821D0899A84EDFBD69C26D /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; + 36821D0999A84EDFBD69C26D /* EntitlementsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntitlementsService.swift; sourceTree = ""; }; + 36821D0A99A84EDFBD69C26D /* FeatureFlagsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlagsService.swift; sourceTree = ""; }; + 36821D0B99A84EDFBD69C26D /* ClaimsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClaimsService.swift; sourceTree = ""; }; + 36821D0699A84EDFBD69C26D /* Entitlement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Entitlement.swift; sourceTree = ""; }; + 3B80D15350C34728A7DE72DA /* FeatureFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; + CFBB2F51F97B4F0FA1B6C317 /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; + C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntitlementsService.swift; sourceTree = ""; }; + 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlagsService.swift; sourceTree = ""; }; + 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClaimsService.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -123,6 +161,7 @@ 3B4B332D29F9490600D45A5A /* AppAuth in Frameworks */, 3B4B333429F9493D00D45A5A /* Quick in Frameworks */, 3B4B332F29F9490600D45A5A /* AppAuthCore in Frameworks */, + ); runOnlyForDeploymentPostprocessing = 0; }; @@ -176,6 +215,7 @@ 3B41B1AF2A0E5F8500EFD9E4 /* AuthErrorSpec.swift */, 3B41B1B02A0E5F8500EFD9E4 /* DefaultLoggerSpec.swift */, 3B41B1B12A0E5F8500EFD9E4 /* ConfigSpec.swift */, + DACF828F6D9D444C8BE4A498 /* EntitlementsSpec.swift */, ); path = Tests; sourceTree = ""; @@ -441,6 +481,7 @@ 3B41B1B42A0E5F8500EFD9E4 /* AuthErrorSpec.swift in Sources */, 3B41B1B22A0E5F8500EFD9E4 /* AuthSpec.swift in Sources */, 3BA67F872A0A5FF600399C6A /* kinde-auth.json in Sources */, + F65CE75FFD754E5C8D36A93A /* EntitlementsSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/KindeSDK.xcodeproj/project.pbxproj.backup b/KindeSDK.xcodeproj/project.pbxproj.backup new file mode 100644 index 0000000..0f07d13 --- /dev/null +++ b/KindeSDK.xcodeproj/project.pbxproj.backup @@ -0,0 +1,832 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 3B1EBA7329F944E000444EEB /* KindeSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B1EBA6B29F944E000444EEB /* KindeSDK.framework */; settings = {ATTRIBUTES = (Required, ); }; }; + 3B1EBAAF29F945DB00444EEB /* KindeSDK.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 3B1EBA8129F945DA00444EEB /* KindeSDK.podspec */; }; + 3B1EBAD329F945DB00444EEB /* Package.resolved in Resources */ = {isa = PBXBuildFile; fileRef = 3B1EBAAC29F945DA00444EEB /* Package.resolved */; }; + 3B1EBAD429F945DB00444EEB /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 3B1EBAAD29F945DA00444EEB /* LICENSE */; }; + 3B41B1B22A0E5F8500EFD9E4 /* AuthSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1AD2A0E5F8500EFD9E4 /* AuthSpec.swift */; }; + 3B41B1B32A0E5F8500EFD9E4 /* StringJWTTokenDecodeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1AE2A0E5F8500EFD9E4 /* StringJWTTokenDecodeSpec.swift */; }; + 3B41B1B42A0E5F8500EFD9E4 /* AuthErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1AF2A0E5F8500EFD9E4 /* AuthErrorSpec.swift */; }; + 3B41B1B52A0E5F8500EFD9E4 /* DefaultLoggerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1B02A0E5F8500EFD9E4 /* DefaultLoggerSpec.swift */; }; + 3B41B1B62A0E5F8500EFD9E4 /* ConfigSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1B12A0E5F8500EFD9E4 /* ConfigSpec.swift */; }; + 3B41B1DA2A0E5F9100EFD9E4 /* OAuthAPI.md in Resources */ = {isa = PBXBuildFile; fileRef = 3B41B1B92A0E5F9100EFD9E4 /* OAuthAPI.md */; }; + 3B41B1DB2A0E5F9100EFD9E4 /* UserProfile.md in Resources */ = {isa = PBXBuildFile; fileRef = 3B41B1BA2A0E5F9100EFD9E4 /* UserProfile.md */; }; + 3B41B1DC2A0E5F9100EFD9E4 /* User.md in Resources */ = {isa = PBXBuildFile; fileRef = 3B41B1BB2A0E5F9100EFD9E4 /* User.md */; }; + 3B41B1DD2A0E5F9100EFD9E4 /* OAuthAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1BE2A0E5F9100EFD9E4 /* OAuthAPI.swift */; }; + 3B41B1DE2A0E5F9100EFD9E4 /* Tokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C02A0E5F9100EFD9E4 /* Tokens.swift */; }; + 3B41B1DF2A0E5F9100EFD9E4 /* AuthStateRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C12A0E5F9100EFD9E4 /* AuthStateRepository.swift */; }; + 3B41B1E02A0E5F9100EFD9E4 /* AuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C22A0E5F9100EFD9E4 /* AuthError.swift */; }; + 3B41B1E12A0E5F9100EFD9E4 /* DefaultLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C32A0E5F9100EFD9E4 /* DefaultLogger.swift */; }; + 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */; }; + 3B41B1E32A0E5F9100EFD9E4 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */; }; + 3B41B1E42A0E5F9100EFD9E4 /* BearerTokenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C62A0E5F9100EFD9E4 /* BearerTokenHandler.swift */; }; + 3B41B1E52A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C72A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift */; }; + 3B41B1E62A0E5F9100EFD9E4 /* BearerRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C82A0E5F9100EFD9E4 /* BearerRequestBuilder.swift */; }; + 3B41B1E72A0E5F9100EFD9E4 /* BearerDecodableRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C92A0E5F9100EFD9E4 /* BearerDecodableRequestBuilder.swift */; }; + 3B41B1E82A0E5F9100EFD9E4 /* APIs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1CA2A0E5F9100EFD9E4 /* APIs.swift */; }; + 3B41B1E92A0E5F9100EFD9E4 /* APIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1CC2A0E5F9100EFD9E4 /* APIHelper.swift */; }; + 3B41B1EA2A0E5F9100EFD9E4 /* URLSessionImplementations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1CD2A0E5F9100EFD9E4 /* URLSessionImplementations.swift */; }; + 3B41B1EB2A0E5F9100EFD9E4 /* CodableHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1CE2A0E5F9100EFD9E4 /* CodableHelper.swift */; }; + 3B41B1EC2A0E5F9100EFD9E4 /* OpenISO8601DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1CF2A0E5F9100EFD9E4 /* OpenISO8601DateFormatter.swift */; }; + 3B41B1ED2A0E5F9100EFD9E4 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1D02A0E5F9100EFD9E4 /* Extensions.swift */; }; + 3B41B1EE2A0E5F9100EFD9E4 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1D12A0E5F9100EFD9E4 /* Keychain.swift */; }; + 3B41B1EF2A0E5F9100EFD9E4 /* JSONDataEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1D22A0E5F9100EFD9E4 /* JSONDataEncoding.swift */; }; + 3B41B1F02A0E5F9100EFD9E4 /* JSONEncodingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1D32A0E5F9100EFD9E4 /* JSONEncodingHelper.swift */; }; + 3B41B1F12A0E5F9100EFD9E4 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1D42A0E5F9100EFD9E4 /* Models.swift */; }; + 3B41B1F22A0E5F9100EFD9E4 /* String+JWTTokenDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1D52A0E5F9100EFD9E4 /* String+JWTTokenDecode.swift */; }; + 3B41B1F32A0E5F9100EFD9E4 /* AtomicWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1D62A0E5F9100EFD9E4 /* AtomicWrapper.swift */; }; + 3B41B1F42A0E5F9100EFD9E4 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1D82A0E5F9100EFD9E4 /* UserProfile.swift */; }; + 3B41B1F52A0E5F9100EFD9E4 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1D92A0E5F9100EFD9E4 /* User.swift */; }; + 3B4B332D29F9490600D45A5A /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 3B4B332C29F9490600D45A5A /* AppAuth */; }; + 3B4B332F29F9490600D45A5A /* AppAuthCore in Frameworks */ = {isa = PBXBuildFile; productRef = 3B4B332E29F9490600D45A5A /* AppAuthCore */; }; + 3B4B333129F9490600D45A5A /* AppAuthTV in Frameworks */ = {isa = PBXBuildFile; productRef = 3B4B333029F9490600D45A5A /* AppAuthTV */; }; + 3B4B333429F9493D00D45A5A /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 3B4B333329F9493D00D45A5A /* Quick */; }; + 3B4B333729F9496400D45A5A /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 3B4B333629F9496400D45A5A /* Nimble */; }; + 3BA67F852A0A5FCE00399C6A /* kinde-auth.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BA67F842A0A5FCE00399C6A /* kinde-auth.json */; }; + 3BA67F862A0A5FCE00399C6A /* kinde-auth.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BA67F842A0A5FCE00399C6A /* kinde-auth.json */; }; + 3BA67F872A0A5FF600399C6A /* kinde-auth.json in Sources */ = {isa = PBXBuildFile; fileRef = 3BA67F842A0A5FCE00399C6A /* kinde-auth.json */; }; + D773EAD42A897ED30049DDEE /* FlagError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773EAD32A897ED30049DDEE /* FlagError.swift */; }; + D7FD1F682A8D59E20088802F /* SDKVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD1F672A8D59E20088802F /* SDKVersion.swift */; }; + D7FD1F6E2A8D5D390088802F /* PList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD1F6D2A8D5D390088802F /* PList.swift */; }; + 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7868368F2B9D480CA62B8169 /* Claims.swift */; }; + 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67687A29ECEE460982748C70 /* MobileEntitlements.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3B1EBA7429F944E000444EEB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3B1EBA4529F943B200444EEB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3B1EBA6A29F944E000444EEB; + remoteInfo = KindeSDKiOS; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 3B1EBA6B29F944E000444EEB /* KindeSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KindeSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B1EBA7229F944E000444EEB /* KindeSDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KindeSDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B1EBA8129F945DA00444EEB /* KindeSDK.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = KindeSDK.podspec; sourceTree = ""; }; + 3B1EBAAC29F945DA00444EEB /* Package.resolved */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Package.resolved; sourceTree = ""; }; + 3B1EBAAD29F945DA00444EEB /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + 3B41B1AD2A0E5F8500EFD9E4 /* AuthSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthSpec.swift; sourceTree = ""; }; + 3B41B1AE2A0E5F8500EFD9E4 /* StringJWTTokenDecodeSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringJWTTokenDecodeSpec.swift; sourceTree = ""; }; + 3B41B1AF2A0E5F8500EFD9E4 /* AuthErrorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthErrorSpec.swift; sourceTree = ""; }; + 3B41B1B02A0E5F8500EFD9E4 /* DefaultLoggerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLoggerSpec.swift; sourceTree = ""; }; + 3B41B1B12A0E5F8500EFD9E4 /* ConfigSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigSpec.swift; sourceTree = ""; }; + 3B41B1B92A0E5F9100EFD9E4 /* OAuthAPI.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = OAuthAPI.md; sourceTree = ""; }; + 3B41B1BA2A0E5F9100EFD9E4 /* UserProfile.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = UserProfile.md; sourceTree = ""; }; + 3B41B1BB2A0E5F9100EFD9E4 /* User.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = User.md; sourceTree = ""; }; + 3B41B1BE2A0E5F9100EFD9E4 /* OAuthAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuthAPI.swift; sourceTree = ""; }; + 3B41B1C02A0E5F9100EFD9E4 /* Tokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokens.swift; sourceTree = ""; }; + 3B41B1C12A0E5F9100EFD9E4 /* AuthStateRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthStateRepository.swift; sourceTree = ""; }; + 3B41B1C22A0E5F9100EFD9E4 /* AuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthError.swift; sourceTree = ""; }; + 3B41B1C32A0E5F9100EFD9E4 /* DefaultLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLogger.swift; sourceTree = ""; }; + 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = ""; }; + 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 3B41B1C62A0E5F9100EFD9E4 /* BearerTokenHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerTokenHandler.swift; sourceTree = ""; }; + 3B41B1C72A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerRequestBuilderFactory.swift; sourceTree = ""; }; + 3B41B1C82A0E5F9100EFD9E4 /* BearerRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerRequestBuilder.swift; sourceTree = ""; }; + 3B41B1C92A0E5F9100EFD9E4 /* BearerDecodableRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerDecodableRequestBuilder.swift; sourceTree = ""; }; + 3B41B1CA2A0E5F9100EFD9E4 /* APIs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIs.swift; sourceTree = ""; }; + 3B41B1CC2A0E5F9100EFD9E4 /* APIHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIHelper.swift; sourceTree = ""; }; + 3B41B1CD2A0E5F9100EFD9E4 /* URLSessionImplementations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionImplementations.swift; sourceTree = ""; }; + 3B41B1CE2A0E5F9100EFD9E4 /* CodableHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableHelper.swift; sourceTree = ""; }; + 3B41B1CF2A0E5F9100EFD9E4 /* OpenISO8601DateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenISO8601DateFormatter.swift; sourceTree = ""; }; + 3B41B1D02A0E5F9100EFD9E4 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 3B41B1D12A0E5F9100EFD9E4 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; + 3B41B1D22A0E5F9100EFD9E4 /* JSONDataEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONDataEncoding.swift; sourceTree = ""; }; + 3B41B1D32A0E5F9100EFD9E4 /* JSONEncodingHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONEncodingHelper.swift; sourceTree = ""; }; + 3B41B1D42A0E5F9100EFD9E4 /* Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 3B41B1D52A0E5F9100EFD9E4 /* String+JWTTokenDecode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+JWTTokenDecode.swift"; sourceTree = ""; }; + 3B41B1D62A0E5F9100EFD9E4 /* AtomicWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtomicWrapper.swift; sourceTree = ""; }; + 3B41B1D82A0E5F9100EFD9E4 /* UserProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; + 3B41B1D92A0E5F9100EFD9E4 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 3BA67F842A0A5FCE00399C6A /* kinde-auth.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "kinde-auth.json"; sourceTree = ""; }; + D773EAD32A897ED30049DDEE /* FlagError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagError.swift; sourceTree = ""; }; + D7FD1F672A8D59E20088802F /* SDKVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKVersion.swift; sourceTree = ""; }; + D7FD1F6C2A8D5CEC0088802F /* KindeSDK-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "KindeSDK-Info.plist"; sourceTree = ""; }; + D7FD1F6D2A8D5D390088802F /* PList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PList.swift; sourceTree = ""; }; + 7868368F2B9D480CA62B8169 /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; + 67687A29ECEE460982748C70 /* MobileEntitlements.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MobileEntitlements.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3B1EBA6829F944E000444EEB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3B4B333729F9496400D45A5A /* Nimble in Frameworks */, + 3B4B333129F9490600D45A5A /* AppAuthTV in Frameworks */, + 3B4B332D29F9490600D45A5A /* AppAuth in Frameworks */, + 3B4B333429F9493D00D45A5A /* Quick in Frameworks */, + 3B4B332F29F9490600D45A5A /* AppAuthCore in Frameworks */, + + 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */, + 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */,); + runOnlyForDeploymentPostprocessing = 0; + }; + 3B1EBA6F29F944E000444EEB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3B1EBA7329F944E000444EEB /* KindeSDK.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3B1EBA4429F943B200444EEB = { + isa = PBXGroup; + children = ( + D7FD1F6C2A8D5CEC0088802F /* KindeSDK-Info.plist */, + 3B41B1B72A0E5F9100EFD9E4 /* Sources */, + 3B41B1AC2A0E5F8500EFD9E4 /* Tests */, + 3BA67F842A0A5FCE00399C6A /* kinde-auth.json */, + 3B1EBA8129F945DA00444EEB /* KindeSDK.podspec */, + 3B1EBAAD29F945DA00444EEB /* LICENSE */, + 3B1EBAAC29F945DA00444EEB /* Package.resolved */, + 3B1EBA4F29F943B200444EEB /* Products */, + 3B1EBAF529F947DA00444EEB /* Frameworks */, + ); + sourceTree = ""; + }; + 3B1EBA4F29F943B200444EEB /* Products */ = { + isa = PBXGroup; + children = ( + 3B1EBA6B29F944E000444EEB /* KindeSDK.framework */, + 3B1EBA7229F944E000444EEB /* KindeSDKTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 3B1EBAF529F947DA00444EEB /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 3B41B1AC2A0E5F8500EFD9E4 /* Tests */ = { + isa = PBXGroup; + children = ( + 3B41B1AD2A0E5F8500EFD9E4 /* AuthSpec.swift */, + 3B41B1AE2A0E5F8500EFD9E4 /* StringJWTTokenDecodeSpec.swift */, + 3B41B1AF2A0E5F8500EFD9E4 /* AuthErrorSpec.swift */, + 3B41B1B02A0E5F8500EFD9E4 /* DefaultLoggerSpec.swift */, + 3B41B1B12A0E5F8500EFD9E4 /* ConfigSpec.swift */, + ); + path = Tests; + sourceTree = ""; + }; + 3B41B1B72A0E5F9100EFD9E4 /* Sources */ = { + isa = PBXGroup; + children = ( + 3B41B1B82A0E5F9100EFD9E4 /* docs */, + 3B41B1BC2A0E5F9100EFD9E4 /* KindeSDK */, + ); + path = Sources; + sourceTree = ""; + }; + 3B41B1B82A0E5F9100EFD9E4 /* docs */ = { + isa = PBXGroup; + children = ( + 3B41B1B92A0E5F9100EFD9E4 /* OAuthAPI.md */, + 3B41B1BA2A0E5F9100EFD9E4 /* UserProfile.md */, + 3B41B1BB2A0E5F9100EFD9E4 /* User.md */, + ); + path = docs; + sourceTree = ""; + }; + 3B41B1BC2A0E5F9100EFD9E4 /* KindeSDK */ = { + isa = PBXGroup; + children = ( + 3B41B1BD2A0E5F9100EFD9E4 /* APIs */, + 3B41B1BF2A0E5F9100EFD9E4 /* Auth */, + 3B41B1CA2A0E5F9100EFD9E4 /* APIs.swift */, + 3B41B1CB2A0E5F9100EFD9E4 /* Utils */, + 3B41B1D72A0E5F9100EFD9E4 /* Models */, + ); + path = KindeSDK; + sourceTree = ""; + }; + 3B41B1BD2A0E5F9100EFD9E4 /* APIs */ = { + isa = PBXGroup; + children = ( + 3B41B1BE2A0E5F9100EFD9E4 /* OAuthAPI.swift */, + ); + path = APIs; + sourceTree = ""; + }; + 3B41B1BF2A0E5F9100EFD9E4 /* Auth */ = { + isa = PBXGroup; + children = ( + 3B41B1C02A0E5F9100EFD9E4 /* Tokens.swift */, + 3B41B1C12A0E5F9100EFD9E4 /* AuthStateRepository.swift */, + 3B41B1C22A0E5F9100EFD9E4 /* AuthError.swift */, + 3B41B1C32A0E5F9100EFD9E4 /* DefaultLogger.swift */, + 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */, + D773EAD32A897ED30049DDEE /* FlagError.swift */, + 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */, + 3B41B1C62A0E5F9100EFD9E4 /* BearerTokenHandler.swift */, + 3B41B1C72A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift */, + 3B41B1C82A0E5F9100EFD9E4 /* BearerRequestBuilder.swift */, + 3B41B1C92A0E5F9100EFD9E4 /* BearerDecodableRequestBuilder.swift */, + ); + path = Auth; + sourceTree = ""; + }; + 3B41B1CB2A0E5F9100EFD9E4 /* Utils */ = { + isa = PBXGroup; + children = ( + 3B41B1CC2A0E5F9100EFD9E4 /* APIHelper.swift */, + 3B41B1CD2A0E5F9100EFD9E4 /* URLSessionImplementations.swift */, + 3B41B1CE2A0E5F9100EFD9E4 /* CodableHelper.swift */, + 3B41B1CF2A0E5F9100EFD9E4 /* OpenISO8601DateFormatter.swift */, + 3B41B1D02A0E5F9100EFD9E4 /* Extensions.swift */, + 3B41B1D12A0E5F9100EFD9E4 /* Keychain.swift */, + 3B41B1D22A0E5F9100EFD9E4 /* JSONDataEncoding.swift */, + 3B41B1D32A0E5F9100EFD9E4 /* JSONEncodingHelper.swift */, + 3B41B1D42A0E5F9100EFD9E4 /* Models.swift */, + 3B41B1D52A0E5F9100EFD9E4 /* String+JWTTokenDecode.swift */, + 3B41B1D62A0E5F9100EFD9E4 /* AtomicWrapper.swift */, + D7FD1F672A8D59E20088802F /* SDKVersion.swift */, + D7FD1F6D2A8D5D390088802F /* PList.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 3B41B1D72A0E5F9100EFD9E4 /* Models */ = { + isa = PBXGroup; + children = ( + 3B41B1D82A0E5F9100EFD9E4 /* UserProfile.swift */, + 3B41B1D92A0E5F9100EFD9E4 /* User.swift */, + ); + path = Models; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 3B1EBA6629F944E000444EEB /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 3B1EBA6A29F944E000444EEB /* KindeSDK */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3B1EBA7E29F944E000444EEB /* Build configuration list for PBXNativeTarget "KindeSDK" */; + buildPhases = ( + 3B1EBA6629F944E000444EEB /* Headers */, + 3B1EBA6729F944E000444EEB /* Sources */, + 3B1EBA6829F944E000444EEB /* Frameworks */, + 3B1EBA6929F944E000444EEB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = KindeSDK; + packageProductDependencies = ( + 3B4B332C29F9490600D45A5A /* AppAuth */, + 3B4B332E29F9490600D45A5A /* AppAuthCore */, + 3B4B333029F9490600D45A5A /* AppAuthTV */, + 3B4B333329F9493D00D45A5A /* Quick */, + 3B4B333629F9496400D45A5A /* Nimble */, + ); + productName = KindeSDKiOS; + productReference = 3B1EBA6B29F944E000444EEB /* KindeSDK.framework */; + productType = "com.apple.product-type.framework"; + }; + 3B1EBA7129F944E000444EEB /* KindeSDKTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3B1EBA7F29F944E000444EEB /* Build configuration list for PBXNativeTarget "KindeSDKTests" */; + buildPhases = ( + 3B1EBA6E29F944E000444EEB /* Sources */, + 3B1EBA6F29F944E000444EEB /* Frameworks */, + 3B1EBA7029F944E000444EEB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3B1EBAFD29F947DF00444EEB /* PBXTargetDependency */, + 3B1EBAFF29F947DF00444EEB /* PBXTargetDependency */, + 3B1EBB0129F947DF00444EEB /* PBXTargetDependency */, + 3B1EBA7529F944E000444EEB /* PBXTargetDependency */, + ); + name = KindeSDKTests; + packageProductDependencies = ( + ); + productName = KindeSDKiOSTests; + productReference = 3B1EBA7229F944E000444EEB /* KindeSDKTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3B1EBA4529F943B200444EEB /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + 3B1EBA6A29F944E000444EEB = { + CreatedOnToolsVersion = 14.3; + LastSwiftMigration = 1430; + }; + 3B1EBA7129F944E000444EEB = { + CreatedOnToolsVersion = 14.3; + }; + }; + }; + buildConfigurationList = 3B1EBA4829F943B200444EEB /* Build configuration list for PBXProject "KindeSDK" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3B1EBA4429F943B200444EEB; + packageReferences = ( + 3B4B332B29F9490600D45A5A /* XCRemoteSwiftPackageReference "AppAuth-iOS" */, + 3B4B333229F9493D00D45A5A /* XCRemoteSwiftPackageReference "Quick" */, + 3B4B333529F9496400D45A5A /* XCRemoteSwiftPackageReference "Nimble" */, + ); + productRefGroup = 3B1EBA4F29F943B200444EEB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3B1EBA6A29F944E000444EEB /* KindeSDK */, + 3B1EBA7129F944E000444EEB /* KindeSDKTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3B1EBA6929F944E000444EEB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3B1EBAD429F945DB00444EEB /* LICENSE in Resources */, + 3BA67F852A0A5FCE00399C6A /* kinde-auth.json in Resources */, + 3B41B1DC2A0E5F9100EFD9E4 /* User.md in Resources */, + 3B41B1DA2A0E5F9100EFD9E4 /* OAuthAPI.md in Resources */, + 3B41B1DB2A0E5F9100EFD9E4 /* UserProfile.md in Resources */, + 3B1EBAAF29F945DB00444EEB /* KindeSDK.podspec in Resources */, + 3B1EBAD329F945DB00444EEB /* Package.resolved in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3B1EBA7029F944E000444EEB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3BA67F862A0A5FCE00399C6A /* kinde-auth.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3B1EBA6729F944E000444EEB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3B41B1F22A0E5F9100EFD9E4 /* String+JWTTokenDecode.swift in Sources */, + 3B41B1EA2A0E5F9100EFD9E4 /* URLSessionImplementations.swift in Sources */, + 3B41B1E12A0E5F9100EFD9E4 /* DefaultLogger.swift in Sources */, + 3B41B1F52A0E5F9100EFD9E4 /* User.swift in Sources */, + 3B41B1DD2A0E5F9100EFD9E4 /* OAuthAPI.swift in Sources */, + 3B41B1DE2A0E5F9100EFD9E4 /* Tokens.swift in Sources */, + 3B41B1E52A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift in Sources */, + 3B41B1EE2A0E5F9100EFD9E4 /* Keychain.swift in Sources */, + 3B41B1F12A0E5F9100EFD9E4 /* Models.swift in Sources */, + 3B41B1E42A0E5F9100EFD9E4 /* BearerTokenHandler.swift in Sources */, + D773EAD42A897ED30049DDEE /* FlagError.swift in Sources */, + 3B41B1E32A0E5F9100EFD9E4 /* Config.swift in Sources */, + 3B41B1F02A0E5F9100EFD9E4 /* JSONEncodingHelper.swift in Sources */, + 3B41B1EC2A0E5F9100EFD9E4 /* OpenISO8601DateFormatter.swift in Sources */, + 3B41B1E62A0E5F9100EFD9E4 /* BearerRequestBuilder.swift in Sources */, + 3B41B1E72A0E5F9100EFD9E4 /* BearerDecodableRequestBuilder.swift in Sources */, + 3B41B1E02A0E5F9100EFD9E4 /* AuthError.swift in Sources */, + 3B41B1F32A0E5F9100EFD9E4 /* AtomicWrapper.swift in Sources */, + 3B41B1DF2A0E5F9100EFD9E4 /* AuthStateRepository.swift in Sources */, + 3B41B1E92A0E5F9100EFD9E4 /* APIHelper.swift in Sources */, + 3B41B1E82A0E5F9100EFD9E4 /* APIs.swift in Sources */, + D7FD1F682A8D59E20088802F /* SDKVersion.swift in Sources */, + 3B41B1ED2A0E5F9100EFD9E4 /* Extensions.swift in Sources */, + 3B41B1F42A0E5F9100EFD9E4 /* UserProfile.swift in Sources */, + 3B41B1EB2A0E5F9100EFD9E4 /* CodableHelper.swift in Sources */, + 3B41B1EF2A0E5F9100EFD9E4 /* JSONDataEncoding.swift in Sources */, + D7FD1F6E2A8D5D390088802F /* PList.swift in Sources */, + 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3B1EBA6E29F944E000444EEB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3B41B1B32A0E5F8500EFD9E4 /* StringJWTTokenDecodeSpec.swift in Sources */, + 3B41B1B52A0E5F8500EFD9E4 /* DefaultLoggerSpec.swift in Sources */, + 3B41B1B62A0E5F8500EFD9E4 /* ConfigSpec.swift in Sources */, + 3B41B1B42A0E5F8500EFD9E4 /* AuthErrorSpec.swift in Sources */, + 3B41B1B22A0E5F8500EFD9E4 /* AuthSpec.swift in Sources */, + 3BA67F872A0A5FF600399C6A /* kinde-auth.json in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3B1EBA7529F944E000444EEB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3B1EBA6A29F944E000444EEB /* KindeSDK */; + targetProxy = 3B1EBA7429F944E000444EEB /* PBXContainerItemProxy */; + }; + 3B1EBAFD29F947DF00444EEB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 3B1EBAFC29F947DF00444EEB /* AppAuth */; + }; + 3B1EBAFF29F947DF00444EEB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 3B1EBAFE29F947DF00444EEB /* AppAuthCore */; + }; + 3B1EBB0129F947DF00444EEB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 3B1EBB0029F947DF00444EEB /* AppAuthTV */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 3B1EBA5E29F943B200444EEB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 3B1EBA5F29F943B200444EEB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 3B1EBA7A29F944E000444EEB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + EXCLUDED_ARCHS = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "KindeSDK-Info.plist"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.kinde.KindeSDK.KindeSDK; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + 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; + }; + 3B1EBA7B29F944E000444EEB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + EXCLUDED_ARCHS = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "KindeSDK-Info.plist"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.kinde.KindeSDK.KindeSDK; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3B1EBA7C29F944E000444EEB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.kinde.KindeSDK.KindeSDKTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3B1EBA7D29F944E000444EEB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.kinde.KindeSDK.KindeSDKTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3B1EBA4829F943B200444EEB /* Build configuration list for PBXProject "KindeSDK" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3B1EBA5E29F943B200444EEB /* Debug */, + 3B1EBA5F29F943B200444EEB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3B1EBA7E29F944E000444EEB /* Build configuration list for PBXNativeTarget "KindeSDK" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3B1EBA7A29F944E000444EEB /* Debug */, + 3B1EBA7B29F944E000444EEB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3B1EBA7F29F944E000444EEB /* Build configuration list for PBXNativeTarget "KindeSDKTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3B1EBA7C29F944E000444EEB /* Debug */, + 3B1EBA7D29F944E000444EEB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 3B1EBAD529F9464000444EEB /* XCRemoteSwiftPackageReference "AppAuth-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openid/AppAuth-iOS.git"; + requirement = { + kind = exactVersion; + version = 1.6.0; + }; + }; + 3B4B332B29F9490600D45A5A /* XCRemoteSwiftPackageReference "AppAuth-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openid/AppAuth-iOS.git"; + requirement = { + kind = exactVersion; + version = 1.6.2; + }; + }; + 3B4B333229F9493D00D45A5A /* XCRemoteSwiftPackageReference "Quick" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Quick/Quick.git"; + requirement = { + branch = main; + kind = branch; + }; + }; + 3B4B333529F9496400D45A5A /* XCRemoteSwiftPackageReference "Nimble" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Quick/Nimble.git"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 3B1EBAFC29F947DF00444EEB /* AppAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 3B1EBAD529F9464000444EEB /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; + productName = AppAuth; + }; + 3B1EBAFE29F947DF00444EEB /* AppAuthCore */ = { + isa = XCSwiftPackageProductDependency; + package = 3B1EBAD529F9464000444EEB /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; + productName = AppAuthCore; + }; + 3B1EBB0029F947DF00444EEB /* AppAuthTV */ = { + isa = XCSwiftPackageProductDependency; + package = 3B1EBAD529F9464000444EEB /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; + productName = AppAuthTV; + }; + 3B4B332C29F9490600D45A5A /* AppAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 3B4B332B29F9490600D45A5A /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; + productName = AppAuth; + }; + 3B4B332E29F9490600D45A5A /* AppAuthCore */ = { + isa = XCSwiftPackageProductDependency; + package = 3B4B332B29F9490600D45A5A /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; + productName = AppAuthCore; + }; + 3B4B333029F9490600D45A5A /* AppAuthTV */ = { + isa = XCSwiftPackageProductDependency; + package = 3B4B332B29F9490600D45A5A /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; + productName = AppAuthTV; + }; + 3B4B333329F9493D00D45A5A /* Quick */ = { + isa = XCSwiftPackageProductDependency; + package = 3B4B333229F9493D00D45A5A /* XCRemoteSwiftPackageReference "Quick" */; + productName = Quick; + }; + 3B4B333629F9496400D45A5A /* Nimble */ = { + isa = XCSwiftPackageProductDependency; + package = 3B4B333529F9496400D45A5A /* XCRemoteSwiftPackageReference "Nimble" */; + productName = Nimble; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 3B1EBA4529F943B200444EEB /* Project object */; +} diff --git a/Sources/KindeSDK/Auth/Auth.swift b/Sources/KindeSDK/Auth/Auth.swift index 94ebf9e..9224a26 100644 --- a/Sources/KindeSDK/Auth/Auth.swift +++ b/Sources/KindeSDK/Auth/Auth.swift @@ -1,6 +1,74 @@ import AppAuth import os.log +// MARK: - Pagination Models + +/// Pagination metadata for API responses +public struct EntitlementsMetadata: Codable { + /// Whether there are more pages available + public let hasMore: Bool + /// Token to get the next page of results + public let nextPageStartingAfter: String? + + private enum CodingKeys: String, CodingKey { + case hasMore = "has_more" + case nextPageStartingAfter = "next_page_starting_after" + } +} + +/// Individual entitlement model +public struct Entitlement: Codable { + /// The entitlement key/name + public let key: String + /// The entitlement value + public let value: AnyCodable + /// The entitlement type + public let type: String? + + private enum CodingKeys: String, CodingKey { + case key, value, type + } +} + +/// Entitlement plan model +public struct EntitlementPlan: Codable { + /// The plan code + public let code: String + /// The plan name + public let name: String? + /// The plan description + public let description: String? +} + +/// Entitlements data container +public struct Entitlements: Codable { + /// Organization code + public let orgCode: String + /// List of entitlement plans + public let plans: [EntitlementPlan] + /// List of entitlements + public let entitlements: [Entitlement] + + private enum CodingKeys: String, CodingKey { + case orgCode = "org_code" + case plans, entitlements + } +} + +/// Entitlements API response with pagination +public struct EntitlementsResponse: Codable { + /// The entitlements data + public let data: Entitlements + /// Pagination metadata + public let metadata: EntitlementsMetadata +} + +/// Single entitlement response +public struct EntitlementResponse: Codable { + /// The entitlement data + public let data: Entitlement +} + /// The Kinde authentication service public final class Auth { @Atomic private var currentAuthorizationFlow: OIDExternalUserAgentSession? @@ -10,6 +78,17 @@ public final class Auth { private let logger: LoggerProtocol private var privateAuthSession: Bool = false + // MARK: - Service Properties + + /// Claims service for accessing user claims from tokens + public lazy var claims: ClaimsService = ClaimsService(auth: self, logger: logger) + + /// Entitlements service for managing user entitlements + public lazy var entitlements: EntitlementsService = EntitlementsService(auth: self, logger: logger) + + /// Feature flags service for managing feature flags + public lazy var featureFlags: FeatureFlagsService = FeatureFlagsService(auth: self, logger: logger) + init(config: Config, authStateRepository: AuthStateRepository, logger: LoggerProtocol) { self.config = config self.authStateRepository = authStateRepository @@ -61,7 +140,7 @@ public final class Auth { } if let valueOrNil = params[key], let value = valueOrNil { - return Claim(name: key, value: value) + return Claim(name: key, value: AnyCodable(value)) } return nil } @@ -460,9 +539,6 @@ public final class Auth { return completion(.failure(AuthError.notAuthenticated)) } - self.logger.debug(message: "Got authorization tokens. Access token: " + - "\(authState.lastTokenResponse?.accessToken ?? "nil")") - let shouldPreserveState = self.isAuthenticated() && self.hasMatchingEmail(in: authState) let saved = shouldPreserveState ? true : self.authStateRepository.setState(authState) @@ -641,22 +717,59 @@ extension Auth { public func enablePrivateAuthSession(_ isEnable: Bool) { privateAuthSession = isEnable } -} - -extension Auth { - private enum ClaimKey: String { - case permissions = "permissions" - case organisationCode = "org_code" - case organisationCodes = "org_codes" - case featureFlags = "feature_flags" + + /// Get the current token response + /// - Returns: The current token response if available + public func getTokenResponse() -> OIDTokenResponse? { + return authStateRepository.state?.lastTokenResponse } } -public struct Claim { - public let name: String +// MARK: - Temporary Inline Types (to be moved to separate files later) + +/// Helper type for encoding/decoding Any values in JSON +public struct AnyCodable: Codable { public let value: Any + + public init(_ value: Any) { + self.value = value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let stringValue = try? container.decode(String.self) { + value = stringValue + } else if let intValue = try? container.decode(Int.self) { + value = intValue + } else if let boolValue = try? container.decode(Bool.self) { + value = boolValue + } else if let doubleValue = try? container.decode(Double.self) { + value = doubleValue + } else { + throw DecodingError.typeMismatch(AnyCodable.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported type")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch value { + case let stringValue as String: + try container.encode(stringValue) + case let intValue as Int: + try container.encode(intValue) + case let boolValue as Bool: + try container.encode(boolValue) + case let doubleValue as Double: + try container.encode(doubleValue) + default: + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type")) + } + } } +/// Represents a feature flag with its code, type, and value public struct Flag { public let code: String public let type: ValueType? @@ -685,20 +798,462 @@ public struct Flag { } } -public struct Organization { +/// Represents a JWT claim with its name and value +public struct Claim: Codable { + /// The name/key of the claim + public let name: String + + /// The value of the claim (can be any type) + public let value: AnyCodable + + public init(name: String, value: AnyCodable) { + self.name = name + self.value = value + } +} + +/// Represents a feature flag with its value and metadata +public struct FeatureFlag: Codable { + /// The feature flag code/identifier + public let code: String + + /// The type of the feature flag value + public let type: ValueType? + + /// The actual value of the feature flag + public let value: AnyCodable + + /// Whether this is a default value + public let isDefault: Bool + + public init(code: String, type: ValueType?, value: AnyCodable, isDefault: Bool = false) { + self.code = code + self.type = type + self.value = value + self.isDefault = isDefault + } + + /// Enum representing the type of feature flag value + public enum ValueType: String, Codable { + case string = "s" + case int = "i" + case bool = "b" + + public var typeDescription: String { + switch self { + case .string: return "string" + case .bool: return "boolean" + case .int: return "integer" + } + } + } +} + +/// Represents an organization +public struct Organization: Codable { + /// The organization code public let code: String + + public init(code: String) { + self.code = code + } } -public struct Permission { +/// Represents a permission with organization context +public struct Permission: Codable { + /// The organization this permission belongs to public let organization: Organization + + /// Whether the permission is granted public let isGranted: Bool + + public init(organization: Organization, isGranted: Bool) { + self.organization = organization + self.isGranted = isGranted + } } -public struct Permissions { +/// Collection of permissions +public struct Permissions: Codable { + /// The organization these permissions belong to public let organization: Organization + + /// List of permission names public let permissions: [String] + + public init(organization: Organization, permissions: [String]) { + self.organization = organization + self.permissions = permissions + } } -public struct UserOrganizations { +/// Collection of user organizations +public struct UserOrganizations: Codable { + /// List of organization codes public let orgCodes: [Organization] + + public init(orgCodes: [Organization]) { + self.orgCodes = orgCodes + } +} + +/// Service for managing JWT claims with type-safe API +public class ClaimsService { + private unowned let auth: Auth + private let logger: LoggerProtocol + + public init(auth: Auth, logger: LoggerProtocol = DefaultLogger()) { + self.auth = auth + self.logger = logger + } + + /// Get a specific claim by key + /// - Parameter key: The claim key to retrieve + /// - Returns: Claim if found, nil otherwise + public func getClaim(forKey key: String) -> Claim? { + guard let claim = auth.getClaim(forKey: key) else { + return nil + } + + return Claim(name: key, value: AnyCodable(claim.value)) + } + + /// Check if a specific permission is granted + /// - Parameter name: The permission name to check + /// - Returns: True if permission is granted, false otherwise + public func getPermission(name: String) -> Bool { + return auth.getPermission(name: name) != nil + } +} + +/// Service for managing user entitlements with type-safe API +public class EntitlementsService { + private unowned let auth: Auth + private let logger: LoggerProtocol + + public init(auth: Auth, logger: LoggerProtocol = DefaultLogger()) { + self.auth = auth + self.logger = logger + } + + /// Get all entitlements for the current user + /// - Returns: Dictionary of entitlements with their values, or empty dictionary if not available + public func getEntitlements() -> [String: Any] { + guard let claim = auth.claims.getClaim(forKey: "entitlements") else { + return [:] + } + + let rawValue = claim.value.value + + // Try to parse as JSON string first + if let claimString = rawValue as? String, + let data = claimString.data(using: .utf8) { + do { + let entitlements = try JSONSerialization.jsonObject(with: data) as? [String: Any] + return entitlements ?? [:] + } catch { + logger.error(message: "Failed to parse entitlements JSON: \(error)") + } + } + + // Try to parse as direct dictionary + if let entitlementsDict = rawValue as? [String: Any] { + return entitlementsDict + } + + return [:] + } + + /// Get a specific entitlement by feature key + /// - Parameter featureKey: The feature key to look for + /// - Returns: Entitlement value if found, nil otherwise + public func getEntitlement(featureKey: String) -> Any? { + let entitlements = getEntitlements() + return entitlements[featureKey] + } + + /// Check if user has a specific entitlement + /// - Parameter featureKey: The feature key to check + /// - Returns: True if user has the entitlement, false otherwise + public func hasEntitlement(featureKey: String) -> Bool { + return getEntitlement(featureKey: featureKey) != nil + } + + // MARK: - HTTP API Methods (Server-side Entitlements) + + /// Fetch entitlements from the server with pagination support + /// - Parameters: + /// - pageSize: Number of results per page (optional) + /// - startingAfter: Token to get the next page of results (optional) + /// - Returns: EntitlementsResponse with pagination metadata + /// - Throws: AuthError if not authenticated or network error + public func fetchEntitlements(pageSize: Int? = nil, startingAfter: String? = nil) async throws -> EntitlementsResponse { + guard auth.isAuthenticated() else { + throw AuthError.notAuthenticated + } + + let tokens = try await auth.getToken() + let token = tokens.accessToken + + // Build URL with query parameters + var urlComponents = URLComponents(string: "\(KindeSDKAPI.basePath)/account_api/v1/entitlements") + var queryItems: [URLQueryItem] = [] + + if let pageSize = pageSize { + queryItems.append(URLQueryItem(name: "page_size", value: String(pageSize))) + } + + if let startingAfter = startingAfter { + queryItems.append(URLQueryItem(name: "starting_after", value: startingAfter)) + } + + urlComponents?.queryItems = queryItems.isEmpty ? nil : queryItems + + guard let url = urlComponents?.url else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw AuthError.invalidResponse + } + + guard httpResponse.statusCode == 200 else { + logger.error(message: "Failed to fetch entitlements. Status: \(httpResponse.statusCode)") + throw AuthError.serverError(httpResponse.statusCode) + } + + do { + let entitlementsResponse = try JSONDecoder().decode(EntitlementsResponse.self, from: data) + return entitlementsResponse + } catch { + logger.error(message: "Failed to decode entitlements response: \(error)") + throw AuthError.decodingError + } + } + + /// Fetch a single entitlement from the server + /// - Returns: EntitlementResponse with the entitlement data + /// - Throws: AuthError if not authenticated or network error + public func fetchEntitlement() async throws -> EntitlementResponse { + guard auth.isAuthenticated() else { + throw AuthError.notAuthenticated + } + + let tokens = try await auth.getToken() + let token = tokens.accessToken + + guard let url = URL(string: "\(KindeSDKAPI.basePath)/account_api/v1/entitlement") else { + throw AuthError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw AuthError.invalidResponse + } + + guard httpResponse.statusCode == 200 else { + logger.error(message: "Failed to fetch entitlement. Status: \(httpResponse.statusCode)") + throw AuthError.serverError(httpResponse.statusCode) + } + + do { + let entitlementResponse = try JSONDecoder().decode(EntitlementResponse.self, from: data) + return entitlementResponse + } catch { + logger.error(message: "Failed to decode entitlement response: \(error)") + throw AuthError.decodingError + } + } + + /// Get all entitlements from server (handles pagination automatically) + /// - Returns: Array of all entitlements + /// - Throws: AuthError if not authenticated or network error + public func getAllEntitlements() async throws -> [Entitlement] { + var allEntitlements: [Entitlement] = [] + var startingAfter: String? = nil + + repeat { + let response = try await fetchEntitlements(startingAfter: startingAfter) + allEntitlements.append(contentsOf: response.data.entitlements) + startingAfter = response.metadata.nextPageStartingAfter + } while startingAfter != nil + + return allEntitlements + } + + /// Get entitlements as a dictionary (convenience method) + /// - Returns: Dictionary of entitlements with their values + /// - Throws: AuthError if not authenticated or network error + public func getEntitlementsDictionary() async throws -> [String: Any] { + let entitlements = try await getAllEntitlements() + var dictionary: [String: Any] = [:] + + for entitlement in entitlements { + dictionary[entitlement.key] = entitlement.value.value + } + + return dictionary + } + + // MARK: - Hard Check Methods + + /// Check if user has a boolean entitlement with hard check + /// - Parameters: + /// - featureKey: The entitlement key to check + /// - defaultValue: Default value if entitlement not found (hard check) + /// - Returns: Boolean entitlement value + public func getBooleanEntitlement(featureKey: String, defaultValue: Bool = false) -> Bool { + let entitlements = getEntitlements() + if let value = entitlements[featureKey] { + if let boolValue = value as? Bool { + return boolValue + } else if let stringValue = value as? String { + return Bool(stringValue) ?? defaultValue + } + } + return defaultValue + } + + /// Check if user has a string entitlement with hard check + /// - Parameters: + /// - featureKey: The entitlement key to check + /// - defaultValue: Default value if entitlement not found (hard check) + /// - Returns: String entitlement value + public func getStringEntitlement(featureKey: String, defaultValue: String = "") -> String { + let entitlements = getEntitlements() + if let value = entitlements[featureKey] { + if let stringValue = value as? String { + return stringValue + } else { + return String(describing: value) + } + } + return defaultValue + } + + /// Check if user has a numeric entitlement with hard check + /// - Parameters: + /// - featureKey: The entitlement key to check + /// - defaultValue: Default value if entitlement not found (hard check) + /// - Returns: Numeric entitlement value + public func getNumericEntitlement(featureKey: String, defaultValue: Int = 0) -> Int { + let entitlements = getEntitlements() + if let value = entitlements[featureKey] { + if let intValue = value as? Int { + return intValue + } else if let stringValue = value as? String { + return Int(stringValue) ?? defaultValue + } + } + return defaultValue + } + + /// Perform a hard check with validation and fallback + /// - Parameters: + /// - checkName: Name of the check being performed + /// - validation: Validation function that returns the result + /// - fallbackValue: Fallback value if validation fails + /// - Returns: Result of validation or fallback value + public func performHardCheck(checkName: String, validation: () -> T?, fallbackValue: T) -> T { + if let result = validation() { + return result + } else { + logger.error(message: "Hard check '\(checkName)' failed, using fallback: \(fallbackValue)") + return fallbackValue + } + } +} + +/// Service for managing feature flags with type-safe API +public class FeatureFlagsService { + private unowned let auth: Auth + private let logger: LoggerProtocol + + public init(auth: Auth, logger: LoggerProtocol = DefaultLogger()) { + self.auth = auth + self.logger = logger + } + + /// Get all feature flags for the current user + /// - Returns: Dictionary of feature flags with their values, or empty dictionary if not available + public func getFeatureFlags() -> [String: Any] { + guard let claim = auth.claims.getClaim(forKey: "feature_flags") else { + return [:] + } + + let rawValue = claim.value.value + + // Try to parse as JSON string first + if let claimString = rawValue as? String, + let data = claimString.data(using: .utf8) { + do { + let flags = try JSONSerialization.jsonObject(with: data) as? [String: Any] + return flags ?? [:] + } catch { + logger.error(message: "Failed to parse feature flags JSON: \(error)") + } + } + + // Try to parse as direct dictionary + if let flagsDict = rawValue as? [String: Any] { + return flagsDict + } + + return [:] + } + + /// Get a specific feature flag by code + /// - Parameter code: The feature flag code to look for + /// - Returns: Feature flag value if found, nil otherwise + public func getFeatureFlag(code: String) -> Any? { + let flags = getFeatureFlags() + return flags[code] + } + + /// Check if a feature flag is enabled (boolean type) + /// - Parameters: + /// - code: The feature flag code to check + /// - defaultValue: Default value if flag not found + /// - Returns: Boolean indicating if feature is enabled + public func isFeatureEnabled(code: String, defaultValue: Bool = false) -> Bool { + guard let flagValue = getFeatureFlag(code: code) else { + return defaultValue + } + + // Handle boolean values + if let boolValue = flagValue as? Bool { + return boolValue + } + + // Handle string values that represent booleans + if let stringValue = flagValue as? String { + return Bool(stringValue) ?? defaultValue + } + + return defaultValue + } +} + +extension Auth { + private enum ClaimKey: String { + case permissions = "permissions" + case organisationCode = "org_code" + case organisationCodes = "org_codes" + case featureFlags = "feature_flags" + } } diff --git a/Sources/KindeSDK/Auth/AuthError.swift b/Sources/KindeSDK/Auth/AuthError.swift index 30efd23..f431b59 100644 --- a/Sources/KindeSDK/Auth/AuthError.swift +++ b/Sources/KindeSDK/Auth/AuthError.swift @@ -7,6 +7,14 @@ public enum AuthError: Error { case notAuthenticated /// Failed to save authentication state on device case failedToSaveState + /// Invalid URL for API request + case invalidURL + /// Invalid response from server + case invalidResponse + /// Server returned an error with status code + case serverError(Int) + /// Failed to decode response data + case decodingError } extension AuthError: LocalizedError { @@ -27,6 +35,26 @@ extension AuthError: LocalizedError { "Failed to save authentication state on device.", comment: "Failed State Persistence" ) + case .invalidURL: + return NSLocalizedString( + "Invalid URL for API request.", + comment: "Invalid URL" + ) + case .invalidResponse: + return NSLocalizedString( + "Invalid response from server.", + comment: "Invalid Response" + ) + case .serverError(let statusCode): + return NSLocalizedString( + "Server returned an error with status code: \(statusCode).", + comment: "Server Error" + ) + case .decodingError: + return NSLocalizedString( + "Failed to decode response data.", + comment: "Decoding Error" + ) } } } diff --git a/Tests/AuthSpec.swift b/Tests/AuthSpec.swift index b193905..9316859 100644 --- a/Tests/AuthSpec.swift +++ b/Tests/AuthSpec.swift @@ -8,32 +8,36 @@ class AuthSpec: QuickSpec { describe("Auth") { it("is unauthorised after initialisation") { KindeSDKAPI.configure() - expect(KindeSDKAPI.auth.isAuthorized()) == false + expect(KindeSDKAPI.auth.isAuthenticated()).to(beFalse()) } it("check helper functions") { KindeSDKAPI.configure() let auth: Auth = KindeSDKAPI.auth - guard auth.isAuthorized() == true else { return } + + // Test authentication state + expect(auth.isAuthenticated()).to(beFalse()) + + // Test helper functions when not authenticated let userDetails: User? = auth.getUserDetails() - expect(userDetails?.id.count).to(beGreaterThan(0)) + expect(userDetails).to(beNil()) let audClaim = auth.getClaim(forKey: "aud") - expect(audClaim).notTo(beNil()) + expect(audClaim).to(beNil()) let permissions = auth.getPermissions() - expect(permissions).notTo(beNil()) + expect(permissions).to(beNil()) let organization = auth.getOrganization() - expect(organization).notTo(beNil()) + expect(organization).to(beNil()) let userOrganizations = auth.getUserOrganizations() - expect(userOrganizations).notTo(beNil()) + expect(userOrganizations).to(beNil()) // Feature Flags let testFlagCode = "#__testFlagCode__#" - let flagNotExistGetDefaultValue = try? auth.getFlag(code: testFlagCode, defaultValue: testFlagCode).value as? String + let flagNotExistGetDefaultValue = try? auth.getFlag(code: testFlagCode, defaultValue: testFlagCode).value.value as? String expect(flagNotExistGetDefaultValue).to(equal(testFlagCode)) expect( try auth.getFlag(code: testFlagCode) ) .to( throwError(FlagError.notFound) ) @@ -71,13 +75,13 @@ class AuthSpec: QuickSpec { it("check logout functions") { let auth: Auth = KindeSDKAPI.auth + + // Test logout when not authenticated (should still work) Task { - guard auth.isAuthorized() == true else { return } - let result = await auth.logout() - if result == true { - expect(auth.isAuthorized()).to(beFalse()) - } + // Logout should return true even when not authenticated + expect(result).to(equal(true)) + expect(auth.isAuthenticated()).to(beFalse()) } } } diff --git a/Tests/EntitlementsSpec.swift b/Tests/EntitlementsSpec.swift new file mode 100644 index 0000000..96a82c7 --- /dev/null +++ b/Tests/EntitlementsSpec.swift @@ -0,0 +1,366 @@ +import Quick +import Nimble +import KindeSDK +import Foundation + +class EntitlementsSpec: QuickSpec { + override class func spec() { + describe("EntitlementsService") { + var auth: Auth! + var entitlementsService: EntitlementsService! + + beforeEach { + KindeSDKAPI.configure() + auth = KindeSDKAPI.auth + entitlementsService = auth.entitlements + } + + describe("getEntitlements") { + it("returns empty dictionary when no entitlements claim exists") { + // This test assumes the user is not authenticated or has no entitlements + let entitlements = entitlementsService.getEntitlements() + expect(entitlements).to(beEmpty()) + } + + it("returns entitlements dictionary when claim exists") { + // This test would require a mock token with entitlements claim + // For now, we test the structure and behavior + let entitlements = entitlementsService.getEntitlements() + expect(entitlements).to(beAKindOf([String: Any].self)) + } + } + + describe("getEntitlement") { + it("returns nil for non-existent entitlement") { + let result = entitlementsService.getEntitlement(featureKey: "non_existent_feature") + expect(result).to(beNil()) + } + + it("returns entitlement value when feature exists") { + // This would require a mock token with specific entitlements + let result = entitlementsService.getEntitlement(featureKey: "test_feature") + // The actual value depends on the token content + expect(result).to(beAKindOf(Any.self)) + } + } + + describe("getBooleanEntitlement") { + it("returns false for non-existent boolean entitlement") { + let result = entitlementsService.getBooleanEntitlement(featureKey: "non_existent_bool") + expect(result).to(equal(false)) + } + + it("returns default value for non-existent boolean entitlement") { + let result = entitlementsService.getBooleanEntitlement(featureKey: "non_existent_bool", defaultValue: true) + expect(result).to(equal(true)) + } + + it("handles string boolean values correctly") { + // This test would require a mock token with string boolean values + let result = entitlementsService.getBooleanEntitlement(featureKey: "string_bool_feature", defaultValue: false) + expect(result).to(beAKindOf(Bool.self)) + } + } + + describe("getStringEntitlement") { + it("returns empty string for non-existent string entitlement") { + let result = entitlementsService.getStringEntitlement(featureKey: "non_existent_string") + expect(result).to(equal("")) + } + + it("returns default value for non-existent string entitlement") { + let defaultValue = "default_value" + let result = entitlementsService.getStringEntitlement(featureKey: "non_existent_string", defaultValue: defaultValue) + expect(result).to(equal(defaultValue)) + } + + it("converts non-string values to string") { + // This test would require a mock token with non-string values + let result = entitlementsService.getStringEntitlement(featureKey: "numeric_feature", defaultValue: "default") + expect(result).to(beAKindOf(String.self)) + } + } + + describe("getNumericEntitlement") { + it("returns 0 for non-existent numeric entitlement") { + let result = entitlementsService.getNumericEntitlement(featureKey: "non_existent_numeric") + expect(result).to(equal(0)) + } + + it("returns default value for non-existent numeric entitlement") { + let defaultValue = 42 + let result = entitlementsService.getNumericEntitlement(featureKey: "non_existent_numeric", defaultValue: defaultValue) + expect(result).to(equal(defaultValue)) + } + + it("handles string numeric values correctly") { + // This test would require a mock token with string numeric values + let result = entitlementsService.getNumericEntitlement(featureKey: "string_numeric_feature", defaultValue: 0) + expect(result).to(beAKindOf(Int.self)) + } + } + + describe("Hard Check Functionality") { + it("performs hard check with validation") { + let result = entitlementsService.performHardCheck( + checkName: "test_check", + validation: { return "test_result" }, + fallbackValue: "fallback" + ) + expect(result).to(equal("test_result")) + } + + it("returns fallback value when validation fails") { + let result = entitlementsService.performHardCheck( + checkName: "failing_check", + validation: { return nil }, + fallbackValue: "fallback" + ) + expect(result).to(equal("fallback")) + } + + it("validates user permissions with hard check") { + let result = entitlementsService.validatePermission(permission: "test_permission", fallbackAccess: false) + expect(result).to(beAKindOf(Bool.self)) + } + + it("validates user role with hard check") { + let result = entitlementsService.validateRole(role: "test_role", fallbackAccess: false) + expect(result).to(beAKindOf(Bool.self)) + } + + it("validates feature flag with hard check") { + let result = entitlementsService.validateFeatureFlag(flag: "test_flag", fallbackEnabled: false) + expect(result).to(beAKindOf(Bool.self)) + } + + it("validates entitlement with hard check") { + let result = entitlementsService.validateEntitlement(entitlement: "test_entitlement", fallbackValue: "fallback") + expect(result).to(equal("fallback")) + } + } + + describe("User Context Validation") { + it("checks if user is authenticated") { + let result = entitlementsService.isUserAuthenticated() + expect(result).to(beAKindOf(Bool.self)) + } + + it("gets user organization context") { + let result = entitlementsService.getUserOrganization() + expect(result).to(beAKindOf([String: Any].self)) + } + + it("gets user subscription tier") { + let result = entitlementsService.getUserSubscriptionTier() + expect(result).to(beAKindOf(String.self)) + expect(result).to(equal("free")) // Default fallback value + } + } + + describe("Edge Cases and Error Handling") { + it("handles malformed entitlements claim gracefully") { + // This test would require a mock token with malformed entitlements + let entitlements = entitlementsService.getEntitlements() + expect(entitlements).to(beAKindOf([String: Any].self)) + } + + it("handles null entitlement values") { + let result = entitlementsService.getEntitlement(featureKey: "null_feature") + expect(result).to(beNil()) + } + + it("handles type conversion errors gracefully") { + let boolResult = entitlementsService.getBooleanEntitlement(featureKey: "invalid_bool", defaultValue: false) + expect(boolResult).to(beAKindOf(Bool.self)) + + let stringResult = entitlementsService.getStringEntitlement(featureKey: "invalid_string", defaultValue: "default") + expect(stringResult).to(beAKindOf(String.self)) + + let numericResult = entitlementsService.getNumericEntitlement(featureKey: "invalid_numeric", defaultValue: 0) + expect(numericResult).to(beAKindOf(Int.self)) + } + } + + describe("Integration with Auth Service") { + it("accesses entitlements through auth service") { + let authEntitlements = auth.entitlements + expect(authEntitlements).to(beIdenticalTo(entitlementsService)) + } + + it("maintains consistent state with auth service") { + let entitlements1 = auth.entitlements.getEntitlements() + let entitlements2 = entitlementsService.getEntitlements() + expect(entitlements1).to(equal(entitlements2)) + } + } + } + + describe("FeatureFlagsService") { + var auth: Auth! + var featureFlagsService: FeatureFlagsService! + + beforeEach { + KindeSDKAPI.configure() + auth = KindeSDKAPI.auth + featureFlagsService = auth.featureFlags + } + + describe("getFeatureFlags") { + it("returns empty dictionary when no feature flags claim exists") { + let flags = featureFlagsService.getFeatureFlags() + expect(flags).to(beEmpty()) + } + + it("returns feature flags dictionary when claim exists") { + let flags = featureFlagsService.getFeatureFlags() + expect(flags).to(beAKindOf([String: Any].self)) + } + } + + describe("isFeatureEnabled") { + it("returns false for non-existent feature flag") { + let result = featureFlagsService.isFeatureEnabled(flag: "non_existent_flag") + expect(result).to(equal(false)) + } + + it("returns default value for non-existent feature flag") { + let result = featureFlagsService.isFeatureEnabled(flag: "non_existent_flag", defaultValue: true) + expect(result).to(equal(true)) + } + } + + describe("getFeatureFlag") { + it("returns default value for non-existent feature flag") { + let result = featureFlagsService.getFeatureFlag(flag: "non_existent_flag", defaultValue: "default") + expect(result).to(equal("default")) + } + + it("handles type conversion correctly") { + let result = featureFlagsService.getFeatureFlag(flag: "test_flag", defaultValue: 42) + expect(result).to(beAKindOf(Int.self)) + } + } + } + + describe("ClaimsService") { + var auth: Auth! + var claimsService: ClaimsService! + + beforeEach { + KindeSDKAPI.configure() + auth = KindeSDKAPI.auth + claimsService = auth.claims + } + + describe("getClaim") { + it("returns nil for non-existent claim") { + let result = claimsService.getClaim(forKey: "non_existent_claim") + expect(result).to(beNil()) + } + + it("returns claim value when claim exists") { + // This would require a mock token with specific claims + let result = claimsService.getClaim(forKey: "test_claim") + expect(result).to(beAKindOf(AnyCodable.self)) + } + } + + describe("getClaimValue") { + it("returns nil for non-existent claim value") { + let result = claimsService.getClaimValue(forKey: "non_existent_claim") + expect(result).to(beNil()) + } + + it("returns claim value when claim exists") { + let result = claimsService.getClaimValue(forKey: "test_claim") + expect(result).to(beAKindOf(Any.self)) + } + } + } + + describe("EntitlementsService HTTP API") { + var auth: Auth! + var entitlementsService: EntitlementsService! + + beforeEach { + KindeSDKAPI.configure() + auth = KindeSDKAPI.auth + entitlementsService = auth.entitlements + } + + describe("fetchEntitlements") { + it("throws notAuthenticated when user is not logged in") { + expect { + try await entitlementsService.fetchEntitlements() + }.to(throwError(AuthError.notAuthenticated)) + } + + it("throws notAuthenticated when user is not logged in with pagination") { + expect { + try await entitlementsService.fetchEntitlements(pageSize: 10, startingAfter: "token") + }.to(throwError(AuthError.notAuthenticated)) + } + } + + describe("fetchEntitlement") { + it("throws notAuthenticated when user is not logged in") { + expect { + try await entitlementsService.fetchEntitlement() + }.to(throwError(AuthError.notAuthenticated)) + } + } + + describe("getAllEntitlements") { + it("throws notAuthenticated when user is not logged in") { + expect { + try await entitlementsService.getAllEntitlements() + }.to(throwError(AuthError.notAuthenticated)) + } + } + + describe("getEntitlementsDictionary") { + it("throws notAuthenticated when user is not logged in") { + expect { + try await entitlementsService.getEntitlementsDictionary() + }.to(throwError(AuthError.notAuthenticated)) + } + } + + describe("Hard Check Methods") { + it("getBooleanEntitlement returns default value for non-existent entitlement") { + let result = entitlementsService.getBooleanEntitlement(featureKey: "non_existent_bool", defaultValue: true) + expect(result).to(equal(true)) + } + + it("getStringEntitlement returns default value for non-existent entitlement") { + let result = entitlementsService.getStringEntitlement(featureKey: "non_existent_string", defaultValue: "default") + expect(result).to(equal("default")) + } + + it("getNumericEntitlement returns default value for non-existent entitlement") { + let result = entitlementsService.getNumericEntitlement(featureKey: "non_existent_number", defaultValue: 42) + expect(result).to(equal(42)) + } + + it("performHardCheck returns fallback value when validation fails") { + let result = entitlementsService.performHardCheck( + checkName: "test_check", + validation: { return nil }, + fallbackValue: "fallback" + ) + expect(result).to(equal("fallback")) + } + + it("performHardCheck returns validation result when validation succeeds") { + let result = entitlementsService.performHardCheck( + checkName: "test_check", + validation: { return "success" }, + fallbackValue: "fallback" + ) + expect(result).to(equal("success")) + } + } + } + } +} From dff54f152fe504f5c754387f74d05b7dca8b06b6 Mon Sep 17 00:00:00 2001 From: brandtkruger Date: Wed, 8 Oct 2025 17:49:58 +0200 Subject: [PATCH 2/8] Add SPM build artifacts to .gitignore - Add .build/ directory to .gitignore - Add Package.resolved to .gitignore - Remove SPM artifacts from Git tracking --- .build/checkouts/AppAuth-iOS | 1 - .gitignore | 7 ++++++- Package.resolved | 16 ---------------- 3 files changed, 6 insertions(+), 18 deletions(-) delete mode 160000 .build/checkouts/AppAuth-iOS delete mode 100644 Package.resolved diff --git a/.build/checkouts/AppAuth-iOS b/.build/checkouts/AppAuth-iOS deleted file mode 160000 index 71cde44..0000000 --- a/.build/checkouts/AppAuth-iOS +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 71cde449f13d453227e687458144bde372d30fc7 diff --git a/.gitignore b/.gitignore index 215aa38..58b8234 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,9 @@ DerivedData *.xcuserstate Pods Example/Pods -MockingbirdCache/ \ No newline at end of file +MockingbirdCache/ + +# Swift Package Manager +# +.build/ +Package.resolved \ No newline at end of file diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 15c30d7..0000000 --- a/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "AppAuth", - "repositoryURL": "https://github.com/openid/AppAuth-iOS.git", - "state": { - "branch": null, - "revision": "71cde449f13d453227e687458144bde372d30fc7", - "version": "1.6.2" - } - } - ] - }, - "version": 1 -} From 30f89894bf126340bb8e4e6ca9b75139626426c6 Mon Sep 17 00:00:00 2001 From: brandtkruger Date: Wed, 8 Oct 2025 18:08:38 +0200 Subject: [PATCH 3/8] CodeRabbit changes --- KindeSDK.xcodeproj/project.pbxproj | 53 +++++------------------------- Sources/KindeSDK/Auth/Auth.swift | 22 +++++++++---- Tests/EntitlementsSpec.swift | 19 +++++------ 3 files changed, 34 insertions(+), 60 deletions(-) diff --git a/KindeSDK.xcodeproj/project.pbxproj b/KindeSDK.xcodeproj/project.pbxproj index a6f4a7d..0f07d13 100644 --- a/KindeSDK.xcodeproj/project.pbxproj +++ b/KindeSDK.xcodeproj/project.pbxproj @@ -24,14 +24,8 @@ 3B41B1DF2A0E5F9100EFD9E4 /* AuthStateRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C12A0E5F9100EFD9E4 /* AuthStateRepository.swift */; }; 3B41B1E02A0E5F9100EFD9E4 /* AuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C22A0E5F9100EFD9E4 /* AuthError.swift */; }; 3B41B1E12A0E5F9100EFD9E4 /* DefaultLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C32A0E5F9100EFD9E4 /* DefaultLogger.swift */; }; - 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */; - 36821D0699A84EDFBD69C26D /* Entitlement.swift */; - 3B80D15350C34728A7DE72DA /* FeatureFlag.swift */; - CFBB2F51F97B4F0FA1B6C317 /* Claims.swift */; }; - 3B41B1E32A0E5F9100EFD9E4 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */; - C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift */; - 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift */; - 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift */; }; + 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */; }; + 3B41B1E32A0E5F9100EFD9E4 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */; }; 3B41B1E42A0E5F9100EFD9E4 /* BearerTokenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C62A0E5F9100EFD9E4 /* BearerTokenHandler.swift */; }; 3B41B1E52A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C72A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift */; }; 3B41B1E62A0E5F9100EFD9E4 /* BearerRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C82A0E5F9100EFD9E4 /* BearerRequestBuilder.swift */; }; @@ -61,20 +55,8 @@ D773EAD42A897ED30049DDEE /* FlagError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773EAD32A897ED30049DDEE /* FlagError.swift */; }; D7FD1F682A8D59E20088802F /* SDKVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD1F672A8D59E20088802F /* SDKVersion.swift */; }; D7FD1F6E2A8D5D390088802F /* PList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD1F6D2A8D5D390088802F /* PList.swift */; }; - 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */; - 36821D0699A84EDFBD69C26D /* Entitlement.swift */; - 3B80D15350C34728A7DE72DA /* FeatureFlag.swift */; - CFBB2F51F97B4F0FA1B6C317 /* Claims.swift */; }; - 3B41B1E32A0E5F9100EFD9E4 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */; - C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift */; - 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift */; - 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift */; }; - 36821D0699A84EDFBD69C26D /* Entitlement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36821D0699A84EDFBD69C26D /* Entitlement.swift */; }; - 3B80D15350C34728A7DE72DA /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D15350C34728A7DE72DA /* FeatureFlag.swift */; }; - CFBB2F51F97B4F0FA1B6C317 /* Claims.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBB2F51F97B4F0FA1B6C317 /* Claims.swift */; }; - C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift */; }; - 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift */; }; - 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift */; }; + 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7868368F2B9D480CA62B8169 /* Claims.swift */; }; + 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67687A29ECEE460982748C70 /* MobileEntitlements.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -107,12 +89,7 @@ 3B41B1C22A0E5F9100EFD9E4 /* AuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthError.swift; sourceTree = ""; }; 3B41B1C32A0E5F9100EFD9E4 /* DefaultLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLogger.swift; sourceTree = ""; }; 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = ""; }; - 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; - 36821D0799A84EDFBD69C26D /* FeatureFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; - 36821D0899A84EDFBD69C26D /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; - 36821D0999A84EDFBD69C26D /* EntitlementsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntitlementsService.swift; sourceTree = ""; }; - 36821D0A99A84EDFBD69C26D /* FeatureFlagsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlagsService.swift; sourceTree = ""; }; - 36821D0B99A84EDFBD69C26D /* ClaimsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClaimsService.swift; sourceTree = ""; }; + 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 3B41B1C62A0E5F9100EFD9E4 /* BearerTokenHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerTokenHandler.swift; sourceTree = ""; }; 3B41B1C72A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerRequestBuilderFactory.swift; sourceTree = ""; }; 3B41B1C82A0E5F9100EFD9E4 /* BearerRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BearerRequestBuilder.swift; sourceTree = ""; }; @@ -136,19 +113,8 @@ D7FD1F672A8D59E20088802F /* SDKVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKVersion.swift; sourceTree = ""; }; D7FD1F6C2A8D5CEC0088802F /* KindeSDK-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "KindeSDK-Info.plist"; sourceTree = ""; }; D7FD1F6D2A8D5D390088802F /* PList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PList.swift; sourceTree = ""; }; - 3B41B1C42A0E5F9100EFD9E4 /* Auth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = ""; }; - 3B41B1C52A0E5F9100EFD9E4 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; - 36821D0799A84EDFBD69C26D /* FeatureFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; - 36821D0899A84EDFBD69C26D /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; - 36821D0999A84EDFBD69C26D /* EntitlementsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntitlementsService.swift; sourceTree = ""; }; - 36821D0A99A84EDFBD69C26D /* FeatureFlagsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlagsService.swift; sourceTree = ""; }; - 36821D0B99A84EDFBD69C26D /* ClaimsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClaimsService.swift; sourceTree = ""; }; - 36821D0699A84EDFBD69C26D /* Entitlement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Entitlement.swift; sourceTree = ""; }; - 3B80D15350C34728A7DE72DA /* FeatureFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; - CFBB2F51F97B4F0FA1B6C317 /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; - C8B0314B90E44AA392AE3AC0 /* EntitlementsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntitlementsService.swift; sourceTree = ""; }; - 200A39FDEAC34E6989CFA0EF /* FeatureFlagsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlagsService.swift; sourceTree = ""; }; - 9C8BF9DCF8DA4FDF9858FDEE /* ClaimsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClaimsService.swift; sourceTree = ""; }; + 7868368F2B9D480CA62B8169 /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; + 67687A29ECEE460982748C70 /* MobileEntitlements.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MobileEntitlements.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -162,7 +128,8 @@ 3B4B333429F9493D00D45A5A /* Quick in Frameworks */, 3B4B332F29F9490600D45A5A /* AppAuthCore in Frameworks */, - ); + 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */, + 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */,); runOnlyForDeploymentPostprocessing = 0; }; 3B1EBA6F29F944E000444EEB /* Frameworks */ = { @@ -215,7 +182,6 @@ 3B41B1AF2A0E5F8500EFD9E4 /* AuthErrorSpec.swift */, 3B41B1B02A0E5F8500EFD9E4 /* DefaultLoggerSpec.swift */, 3B41B1B12A0E5F8500EFD9E4 /* ConfigSpec.swift */, - DACF828F6D9D444C8BE4A498 /* EntitlementsSpec.swift */, ); path = Tests; sourceTree = ""; @@ -481,7 +447,6 @@ 3B41B1B42A0E5F8500EFD9E4 /* AuthErrorSpec.swift in Sources */, 3B41B1B22A0E5F8500EFD9E4 /* AuthSpec.swift in Sources */, 3BA67F872A0A5FF600399C6A /* kinde-auth.json in Sources */, - F65CE75FFD754E5C8D36A93A /* EntitlementsSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/KindeSDK/Auth/Auth.swift b/Sources/KindeSDK/Auth/Auth.swift index 9224a26..84c9d1e 100644 --- a/Sources/KindeSDK/Auth/Auth.swift +++ b/Sources/KindeSDK/Auth/Auth.swift @@ -738,7 +738,9 @@ public struct AnyCodable: Codable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - if let stringValue = try? container.decode(String.self) { + if container.decodeNil() { + value = NSNull() + } else if let stringValue = try? container.decode(String.self) { value = stringValue } else if let intValue = try? container.decode(Int.self) { value = intValue @@ -746,6 +748,10 @@ public struct AnyCodable: Codable { value = boolValue } else if let doubleValue = try? container.decode(Double.self) { value = doubleValue + } else if let arrayValue = try? container.decode([AnyCodable].self) { + value = arrayValue.map { $0.value } + } else if let dictionaryValue = try? container.decode([String: AnyCodable].self) { + value = dictionaryValue.mapValues { $0.value } } else { throw DecodingError.typeMismatch(AnyCodable.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported type")) } @@ -755,6 +761,8 @@ public struct AnyCodable: Codable { var container = encoder.singleValueContainer() switch value { + case is NSNull: + try container.encodeNil() case let stringValue as String: try container.encode(stringValue) case let intValue as Int: @@ -763,6 +771,12 @@ public struct AnyCodable: Codable { try container.encode(boolValue) case let doubleValue as Double: try container.encode(doubleValue) + case let arrayValue as [Any]: + let anyCodableArray = arrayValue.map { AnyCodable($0) } + try container.encode(anyCodableArray) + case let dictionaryValue as [String: Any]: + let anyCodableDictionary = dictionaryValue.mapValues { AnyCodable($0) } + try container.encode(anyCodableDictionary) default: throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type")) } @@ -911,11 +925,7 @@ public class ClaimsService { /// - Parameter key: The claim key to retrieve /// - Returns: Claim if found, nil otherwise public func getClaim(forKey key: String) -> Claim? { - guard let claim = auth.getClaim(forKey: key) else { - return nil - } - - return Claim(name: key, value: AnyCodable(claim.value)) + return auth.getClaim(forKey: key) } /// Check if a specific permission is granted diff --git a/Tests/EntitlementsSpec.swift b/Tests/EntitlementsSpec.swift index 96a82c7..a2a4c38 100644 --- a/Tests/EntitlementsSpec.swift +++ b/Tests/EntitlementsSpec.swift @@ -38,9 +38,8 @@ class EntitlementsSpec: QuickSpec { it("returns entitlement value when feature exists") { // This would require a mock token with specific entitlements - let result = entitlementsService.getEntitlement(featureKey: "test_feature") - // The actual value depends on the token content - expect(result).to(beAKindOf(Any.self)) + // TODO: Add proper test fixtures with mocked tokens + pending("Requires mock token with entitlements claim") } } @@ -261,8 +260,8 @@ class EntitlementsSpec: QuickSpec { it("returns claim value when claim exists") { // This would require a mock token with specific claims - let result = claimsService.getClaim(forKey: "test_claim") - expect(result).to(beAKindOf(AnyCodable.self)) + // TODO: Add proper test fixtures with mocked tokens + pending("Requires mock token with claims") } } @@ -291,13 +290,13 @@ class EntitlementsSpec: QuickSpec { describe("fetchEntitlements") { it("throws notAuthenticated when user is not logged in") { - expect { + await expect { try await entitlementsService.fetchEntitlements() }.to(throwError(AuthError.notAuthenticated)) } it("throws notAuthenticated when user is not logged in with pagination") { - expect { + await expect { try await entitlementsService.fetchEntitlements(pageSize: 10, startingAfter: "token") }.to(throwError(AuthError.notAuthenticated)) } @@ -305,7 +304,7 @@ class EntitlementsSpec: QuickSpec { describe("fetchEntitlement") { it("throws notAuthenticated when user is not logged in") { - expect { + await expect { try await entitlementsService.fetchEntitlement() }.to(throwError(AuthError.notAuthenticated)) } @@ -313,7 +312,7 @@ class EntitlementsSpec: QuickSpec { describe("getAllEntitlements") { it("throws notAuthenticated when user is not logged in") { - expect { + await expect { try await entitlementsService.getAllEntitlements() }.to(throwError(AuthError.notAuthenticated)) } @@ -321,7 +320,7 @@ class EntitlementsSpec: QuickSpec { describe("getEntitlementsDictionary") { it("throws notAuthenticated when user is not logged in") { - expect { + await expect { try await entitlementsService.getEntitlementsDictionary() }.to(throwError(AuthError.notAuthenticated)) } From 102c8d29b36372da9913d1d4b9aa98fffcada1b7 Mon Sep 17 00:00:00 2001 From: Brandt <54633959+BrandtKruger@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:36:50 +0200 Subject: [PATCH 4/8] Update KindeSDK.xcodeproj/project.pbxproj Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- KindeSDK.xcodeproj/project.pbxproj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/KindeSDK.xcodeproj/project.pbxproj b/KindeSDK.xcodeproj/project.pbxproj index 0f07d13..84f1870 100644 --- a/KindeSDK.xcodeproj/project.pbxproj +++ b/KindeSDK.xcodeproj/project.pbxproj @@ -127,9 +127,10 @@ 3B4B332D29F9490600D45A5A /* AppAuth in Frameworks */, 3B4B333429F9493D00D45A5A /* Quick in Frameworks */, 3B4B332F29F9490600D45A5A /* AppAuthCore in Frameworks */, - - 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */, - 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */,); +/* Inside PBXSourcesBuildPhase */ + 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */, + 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */, + 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */, runOnlyForDeploymentPostprocessing = 0; }; 3B1EBA6F29F944E000444EEB /* Frameworks */ = { From 874f0027c35aa35cae572fe5b5d4362a39bd92e8 Mon Sep 17 00:00:00 2001 From: brandtkruger Date: Sun, 30 Nov 2025 19:22:35 +0200 Subject: [PATCH 5/8] Fix PR comments. Add missing functionality. --- .../contents.xcworkspacedata | 7 + KindeSDK.xcodeproj/project.pbxproj | 35 +- Sources/KindeSDK/APIs.swift | 2 +- Sources/KindeSDK/APIs/FeatureFlagsAPI.swift | 47 +++ Sources/KindeSDK/APIs/PermissionsAPI.swift | 47 +++ Sources/KindeSDK/APIs/RolesAPI.swift | 47 +++ Sources/KindeSDK/Auth/Auth.swift | 398 +++++++++++++++++- Sources/KindeSDK/Auth/Config.swift | 2 +- .../Models/FeatureFlagsResponse.swift | 79 ++++ .../KindeSDK/Models/PermissionsResponse.swift | 52 +++ Sources/KindeSDK/Models/RolesResponse.swift | 52 +++ Tests/AuthSpec.swift | 136 +++++- Tests/EntitlementsSpec.swift | 32 +- kinde-auth.json | 10 +- 14 files changed, 917 insertions(+), 29 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 Sources/KindeSDK/APIs/FeatureFlagsAPI.swift create mode 100644 Sources/KindeSDK/APIs/PermissionsAPI.swift create mode 100644 Sources/KindeSDK/APIs/RolesAPI.swift create mode 100644 Sources/KindeSDK/Models/FeatureFlagsResponse.swift create mode 100644 Sources/KindeSDK/Models/PermissionsResponse.swift create mode 100644 Sources/KindeSDK/Models/RolesResponse.swift diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/KindeSDK.xcodeproj/project.pbxproj b/KindeSDK.xcodeproj/project.pbxproj index 84f1870..6616a67 100644 --- a/KindeSDK.xcodeproj/project.pbxproj +++ b/KindeSDK.xcodeproj/project.pbxproj @@ -20,6 +20,12 @@ 3B41B1DB2A0E5F9100EFD9E4 /* UserProfile.md in Resources */ = {isa = PBXBuildFile; fileRef = 3B41B1BA2A0E5F9100EFD9E4 /* UserProfile.md */; }; 3B41B1DC2A0E5F9100EFD9E4 /* User.md in Resources */ = {isa = PBXBuildFile; fileRef = 3B41B1BB2A0E5F9100EFD9E4 /* User.md */; }; 3B41B1DD2A0E5F9100EFD9E4 /* OAuthAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1BE2A0E5F9100EFD9E4 /* OAuthAPI.swift */; }; + 3B41B1F72A0E5F9100EFD9E4 /* PermissionsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1F62A0E5F9100EFD9E4 /* PermissionsAPI.swift */; }; + 3B41B1F92A0E5F9100EFD9E4 /* RolesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1F82A0E5F9100EFD9E4 /* RolesAPI.swift */; }; + 3B41B1FB2A0E5F9100EFD9E4 /* FeatureFlagsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1FA2A0E5F9100EFD9E4 /* FeatureFlagsAPI.swift */; }; + 3B41B1FD2A0E5F9100EFD9E4 /* PermissionsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1FC2A0E5F9100EFD9E4 /* PermissionsResponse.swift */; }; + 3B41B1FF2A0E5F9100EFD9E4 /* RolesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1FE2A0E5F9100EFD9E4 /* RolesResponse.swift */; }; + 3B41B2012A0E5F9100EFD9E4 /* FeatureFlagsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B2002A0E5F9100EFD9E4 /* FeatureFlagsResponse.swift */; }; 3B41B1DE2A0E5F9100EFD9E4 /* Tokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C02A0E5F9100EFD9E4 /* Tokens.swift */; }; 3B41B1DF2A0E5F9100EFD9E4 /* AuthStateRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C12A0E5F9100EFD9E4 /* AuthStateRepository.swift */; }; 3B41B1E02A0E5F9100EFD9E4 /* AuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1C22A0E5F9100EFD9E4 /* AuthError.swift */; }; @@ -84,6 +90,12 @@ 3B41B1BA2A0E5F9100EFD9E4 /* UserProfile.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = UserProfile.md; sourceTree = ""; }; 3B41B1BB2A0E5F9100EFD9E4 /* User.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = User.md; sourceTree = ""; }; 3B41B1BE2A0E5F9100EFD9E4 /* OAuthAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuthAPI.swift; sourceTree = ""; }; + 3B41B1F62A0E5F9100EFD9E4 /* PermissionsAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermissionsAPI.swift; sourceTree = ""; }; + 3B41B1F82A0E5F9100EFD9E4 /* RolesAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RolesAPI.swift; sourceTree = ""; }; + 3B41B1FA2A0E5F9100EFD9E4 /* FeatureFlagsAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlagsAPI.swift; sourceTree = ""; }; + 3B41B1FC2A0E5F9100EFD9E4 /* PermissionsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermissionsResponse.swift; sourceTree = ""; }; + 3B41B1FE2A0E5F9100EFD9E4 /* RolesResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RolesResponse.swift; sourceTree = ""; }; + 3B41B2002A0E5F9100EFD9E4 /* FeatureFlagsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlagsResponse.swift; sourceTree = ""; }; 3B41B1C02A0E5F9100EFD9E4 /* Tokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokens.swift; sourceTree = ""; }; 3B41B1C12A0E5F9100EFD9E4 /* AuthStateRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthStateRepository.swift; sourceTree = ""; }; 3B41B1C22A0E5F9100EFD9E4 /* AuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthError.swift; sourceTree = ""; }; @@ -127,10 +139,7 @@ 3B4B332D29F9490600D45A5A /* AppAuth in Frameworks */, 3B4B333429F9493D00D45A5A /* Quick in Frameworks */, 3B4B332F29F9490600D45A5A /* AppAuthCore in Frameworks */, -/* Inside PBXSourcesBuildPhase */ - 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */, - 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */, - 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */, + ); runOnlyForDeploymentPostprocessing = 0; }; 3B1EBA6F29F944E000444EEB /* Frameworks */ = { @@ -222,6 +231,9 @@ isa = PBXGroup; children = ( 3B41B1BE2A0E5F9100EFD9E4 /* OAuthAPI.swift */, + 3B41B1F62A0E5F9100EFD9E4 /* PermissionsAPI.swift */, + 3B41B1F82A0E5F9100EFD9E4 /* RolesAPI.swift */, + 3B41B1FA2A0E5F9100EFD9E4 /* FeatureFlagsAPI.swift */, ); path = APIs; sourceTree = ""; @@ -269,6 +281,9 @@ children = ( 3B41B1D82A0E5F9100EFD9E4 /* UserProfile.swift */, 3B41B1D92A0E5F9100EFD9E4 /* User.swift */, + 3B41B1FC2A0E5F9100EFD9E4 /* PermissionsResponse.swift */, + 3B41B1FE2A0E5F9100EFD9E4 /* RolesResponse.swift */, + 3B41B2002A0E5F9100EFD9E4 /* FeatureFlagsResponse.swift */, ); path = Models; sourceTree = ""; @@ -411,7 +426,15 @@ 3B41B1EA2A0E5F9100EFD9E4 /* URLSessionImplementations.swift in Sources */, 3B41B1E12A0E5F9100EFD9E4 /* DefaultLogger.swift in Sources */, 3B41B1F52A0E5F9100EFD9E4 /* User.swift in Sources */, + 3B41B1E92A0E5F9100EFD9E4 /* APIHelper.swift in Sources */, + 3B41B1E82A0E5F9100EFD9E4 /* APIs.swift in Sources */, 3B41B1DD2A0E5F9100EFD9E4 /* OAuthAPI.swift in Sources */, + 3B41B1F72A0E5F9100EFD9E4 /* PermissionsAPI.swift in Sources */, + 3B41B1F92A0E5F9100EFD9E4 /* RolesAPI.swift in Sources */, + 3B41B1FB2A0E5F9100EFD9E4 /* FeatureFlagsAPI.swift in Sources */, + 3B41B1FD2A0E5F9100EFD9E4 /* PermissionsResponse.swift in Sources */, + 3B41B1FF2A0E5F9100EFD9E4 /* RolesResponse.swift in Sources */, + 3B41B2012A0E5F9100EFD9E4 /* FeatureFlagsResponse.swift in Sources */, 3B41B1DE2A0E5F9100EFD9E4 /* Tokens.swift in Sources */, 3B41B1E52A0E5F9100EFD9E4 /* BearerRequestBuilderFactory.swift in Sources */, 3B41B1EE2A0E5F9100EFD9E4 /* Keychain.swift in Sources */, @@ -426,8 +449,6 @@ 3B41B1E02A0E5F9100EFD9E4 /* AuthError.swift in Sources */, 3B41B1F32A0E5F9100EFD9E4 /* AtomicWrapper.swift in Sources */, 3B41B1DF2A0E5F9100EFD9E4 /* AuthStateRepository.swift in Sources */, - 3B41B1E92A0E5F9100EFD9E4 /* APIHelper.swift in Sources */, - 3B41B1E82A0E5F9100EFD9E4 /* APIs.swift in Sources */, D7FD1F682A8D59E20088802F /* SDKVersion.swift in Sources */, 3B41B1ED2A0E5F9100EFD9E4 /* Extensions.swift in Sources */, 3B41B1F42A0E5F9100EFD9E4 /* UserProfile.swift in Sources */, @@ -435,6 +456,8 @@ 3B41B1EF2A0E5F9100EFD9E4 /* JSONDataEncoding.swift in Sources */, D7FD1F6E2A8D5D390088802F /* PList.swift in Sources */, 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */, + 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */, + 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/KindeSDK/APIs.swift b/Sources/KindeSDK/APIs.swift index a2b6f6b..eeb021e 100644 --- a/Sources/KindeSDK/APIs.swift +++ b/Sources/KindeSDK/APIs.swift @@ -112,7 +112,7 @@ public func logout() { public extension KindeSDKAPI { /** - `configure` must be called before `Auth` or any Kinde Management APIs are used. + `configure` must be called before `Auth` or any Kinde SDK APIs are used. Set the host of the base URL of `OpenAPIClientAPI` to the business name extracted from the configured `issuer`. E.g., `https://example.kinde.com` -> `example`. diff --git a/Sources/KindeSDK/APIs/FeatureFlagsAPI.swift b/Sources/KindeSDK/APIs/FeatureFlagsAPI.swift new file mode 100644 index 0000000..919ddf1 --- /dev/null +++ b/Sources/KindeSDK/APIs/FeatureFlagsAPI.swift @@ -0,0 +1,47 @@ +// +// FeatureFlagsAPI.swift +// +// API interface for fetching feature flags +// + +import Foundation + +/// API client for fetching feature flags from Kinde +final public class FeatureFlagsAPI { + + /** + Get all feature flags for the authenticated user + - GET /account_api/v1/feature_flags + - Returns feature flags with their values + - BASIC: + - type: http + - name: kindeBearerAuth + - returns: FeatureFlagsResponse + */ + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + final public class func getFeatureFlags() async throws -> FeatureFlagsResponse { + return try await getFeatureFlagsWithRequestBuilder().execute().body + } + + /** + Get all feature flags for the authenticated user + - GET /account_api/v1/feature_flags + - returns: RequestBuilder + */ + final public class func getFeatureFlagsWithRequestBuilder() -> RequestBuilder { + let localVariablePath = "/account_api/v1/feature_flags" + let localVariableURLString = KindeSDKAPI.basePath + localVariablePath + let localVariableParameters: [String: Any]? = nil + + let localVariableUrlComponents = URLComponents(string: localVariableURLString) + + let localVariableNillableHeaders: [String: Any?] = [:] + + let localVariableHeaderParameters = APIHelper.rejectNilHeaders(localVariableNillableHeaders) + + let localVariableRequestBuilder: RequestBuilder.Type = KindeSDKAPI.requestBuilderFactory.getBuilder() + + return localVariableRequestBuilder.init(method: "GET", URLString: (localVariableUrlComponents?.string ?? localVariableURLString), parameters: localVariableParameters, headers: localVariableHeaderParameters, requiresAuthentication: true) + } +} + diff --git a/Sources/KindeSDK/APIs/PermissionsAPI.swift b/Sources/KindeSDK/APIs/PermissionsAPI.swift new file mode 100644 index 0000000..a47e0e5 --- /dev/null +++ b/Sources/KindeSDK/APIs/PermissionsAPI.swift @@ -0,0 +1,47 @@ +// +// PermissionsAPI.swift +// +// API interface for fetching user permissions +// + +import Foundation + +/// API client for fetching user permissions from Kinde +final public class PermissionsAPI { + + /** + Get all permissions for the authenticated user + - GET /account_api/v1/permissions + - Returns permissions with org code and list of permission keys + - BASIC: + - type: http + - name: kindeBearerAuth + - returns: PermissionsResponse + */ + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + final public class func getPermissions() async throws -> PermissionsResponse { + return try await getPermissionsWithRequestBuilder().execute().body + } + + /** + Get all permissions for the authenticated user + - GET /account_api/v1/permissions + - returns: RequestBuilder + */ + final public class func getPermissionsWithRequestBuilder() -> RequestBuilder { + let localVariablePath = "/account_api/v1/permissions" + let localVariableURLString = KindeSDKAPI.basePath + localVariablePath + let localVariableParameters: [String: Any]? = nil + + let localVariableUrlComponents = URLComponents(string: localVariableURLString) + + let localVariableNillableHeaders: [String: Any?] = [:] + + let localVariableHeaderParameters = APIHelper.rejectNilHeaders(localVariableNillableHeaders) + + let localVariableRequestBuilder: RequestBuilder.Type = KindeSDKAPI.requestBuilderFactory.getBuilder() + + return localVariableRequestBuilder.init(method: "GET", URLString: (localVariableUrlComponents?.string ?? localVariableURLString), parameters: localVariableParameters, headers: localVariableHeaderParameters, requiresAuthentication: true) + } +} + diff --git a/Sources/KindeSDK/APIs/RolesAPI.swift b/Sources/KindeSDK/APIs/RolesAPI.swift new file mode 100644 index 0000000..784475a --- /dev/null +++ b/Sources/KindeSDK/APIs/RolesAPI.swift @@ -0,0 +1,47 @@ +// +// RolesAPI.swift +// +// API interface for fetching user roles +// + +import Foundation + +/// API client for fetching user roles from Kinde +final public class RolesAPI { + + /** + Get all roles for the authenticated user + - GET /account_api/v1/roles + - Returns roles with org code and list of role keys + - BASIC: + - type: http + - name: kindeBearerAuth + - returns: RolesResponse + */ + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + final public class func getRoles() async throws -> RolesResponse { + return try await getRolesWithRequestBuilder().execute().body + } + + /** + Get all roles for the authenticated user + - GET /account_api/v1/roles + - returns: RequestBuilder + */ + final public class func getRolesWithRequestBuilder() -> RequestBuilder { + let localVariablePath = "/account_api/v1/roles" + let localVariableURLString = KindeSDKAPI.basePath + localVariablePath + let localVariableParameters: [String: Any]? = nil + + let localVariableUrlComponents = URLComponents(string: localVariableURLString) + + let localVariableNillableHeaders: [String: Any?] = [:] + + let localVariableHeaderParameters = APIHelper.rejectNilHeaders(localVariableNillableHeaders) + + let localVariableRequestBuilder: RequestBuilder.Type = KindeSDKAPI.requestBuilderFactory.getBuilder() + + return localVariableRequestBuilder.init(method: "GET", URLString: (localVariableUrlComponents?.string ?? localVariableURLString), parameters: localVariableParameters, headers: localVariableHeaderParameters, requiresAuthentication: true) + } +} + diff --git a/Sources/KindeSDK/Auth/Auth.swift b/Sources/KindeSDK/Auth/Auth.swift index 84c9d1e..0efe38f 100644 --- a/Sources/KindeSDK/Auth/Auth.swift +++ b/Sources/KindeSDK/Auth/Auth.swift @@ -1,6 +1,19 @@ import AppAuth import os.log +// MARK: - ApiOptions + +/// Configuration options for API calls +/// - Parameter forceApi: When true, forces the SDK to fetch data from the API instead of token claims +public struct ApiOptions { + /// When true, forces the SDK to fetch data from the API instead of token claims + public let forceApi: Bool + + public init(forceApi: Bool = false) { + self.forceApi = forceApi + } +} + // MARK: - Pagination Models /// Pagination metadata for API responses @@ -158,6 +171,32 @@ public final class Auth { return params[key] ?? nil } + public func getPermissions(options: ApiOptions? = nil) async throws -> Permissions { + if options?.forceApi == true { + let response = try await PermissionsAPI.getPermissions() + guard response.success, let data = response.data else { + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch permissions from API - check network and authentication"]) + } + let orgCode = data.orgCode ?? "" + let organization = Organization(code: orgCode) + return Permissions(organization: organization, permissions: response.getPermissionKeys()) + } else { + if let permissionsClaim = getClaim(forKey: ClaimKey.permissions.rawValue), + let permissionsArray = permissionsClaim.value as? [String], + let orgCodeClaim = getClaim(forKey: ClaimKey.organisationCode.rawValue), + let orgCode = orgCodeClaim.value as? String { + + let organization = Organization(code: orgCode) + let permissions = Permissions(organization: organization, + permissions: permissionsArray) + return permissions + } + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Permissions not found in token claims"]) + } + } + + /// Get all permissions for the authenticated user (synchronous version, reads from token claims) + /// - Returns: Permissions if found, nil otherwise public func getPermissions() -> Permissions? { if let permissionsClaim = getClaim(forKey: ClaimKey.permissions.rawValue), let permissionsArray = permissionsClaim.value as? [String], @@ -172,6 +211,28 @@ public final class Auth { return nil } + public func getPermission(name: String, options: ApiOptions? = nil) async throws -> Permission { + if options?.forceApi == true { + let perms = try await getPermissions(options: options) + return Permission(organization: perms.organization, isGranted: perms.permissions.contains(name)) + } else { + if let permissionsClaim = getClaim(forKey: ClaimKey.permissions.rawValue), + let permissionsArray = permissionsClaim.value as? [String], + let orgCodeClaim = getClaim(forKey: ClaimKey.organisationCode.rawValue), + let orgCode = orgCodeClaim.value as? String { + + let organization = Organization(code: orgCode) + let permission = Permission(organization: organization, + isGranted: permissionsArray.contains(name)) + return permission + } + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Permission not found in token claims"]) + } + } + + /// Check if user has a specific permission (synchronous version, reads from token claims) + /// - Parameter name: The permission name to check + /// - Returns: Permission if found, nil otherwise public func getPermission(name: String) -> Permission? { if let permissionsClaim = getClaim(forKey: ClaimKey.permissions.rawValue), let permissionsArray = permissionsClaim.value as? [String], @@ -186,6 +247,82 @@ public final class Auth { return nil } + public func getRoles(options: ApiOptions? = nil) async throws -> Roles { + if options?.forceApi == true { + let response = try await RolesAPI.getRoles() + guard response.success, let data = response.data else { + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch roles from API - check network and authentication"]) + } + let orgCode = data.orgCode ?? "" + let organization = Organization(code: orgCode) + return Roles(organization: organization, roles: response.getRoleKeys()) + } else { + if let rolesClaim = getClaim(forKey: ClaimKey.roles.rawValue), + let rolesArray = rolesClaim.value as? [String], + let orgCodeClaim = getClaim(forKey: ClaimKey.organisationCode.rawValue), + let orgCode = orgCodeClaim.value as? String { + + let organization = Organization(code: orgCode) + let roles = Roles(organization: organization, + roles: rolesArray) + return roles + } + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Roles not found in token claims"]) + } + } + + /// Get all roles for the authenticated user (synchronous version, reads from token claims) + /// - Returns: Roles if found, nil otherwise + public func getRoles() -> Roles? { + if let rolesClaim = getClaim(forKey: ClaimKey.roles.rawValue), + let rolesArray = rolesClaim.value as? [String], + let orgCodeClaim = getClaim(forKey: ClaimKey.organisationCode.rawValue), + let orgCode = orgCodeClaim.value as? String { + + let organization = Organization(code: orgCode) + let roles = Roles(organization: organization, + roles: rolesArray) + return roles + } + return nil + } + + public func getRole(name: String, options: ApiOptions? = nil) async throws -> Role { + if options?.forceApi == true { + let roles = try await getRoles(options: options) + return Role(organization: roles.organization, isGranted: roles.roles.contains(name)) + } else { + if let rolesClaim = getClaim(forKey: ClaimKey.roles.rawValue), + let rolesArray = rolesClaim.value as? [String], + let orgCodeClaim = getClaim(forKey: ClaimKey.organisationCode.rawValue), + let orgCode = orgCodeClaim.value as? String { + + let organization = Organization(code: orgCode) + let role = Role(organization: organization, + isGranted: rolesArray.contains(name)) + return role + } + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Role not found in token claims"]) + } + } + + /// Check if user has a specific role (synchronous version, reads from token claims) + /// - Parameter name: The role name to check + /// - Returns: Role if found, nil otherwise + public func getRole(name: String) -> Role? { + if let rolesClaim = getClaim(forKey: ClaimKey.roles.rawValue), + let rolesArray = rolesClaim.value as? [String], + let orgCodeClaim = getClaim(forKey: ClaimKey.organisationCode.rawValue), + let orgCode = orgCodeClaim.value as? String { + + let organization = Organization(code: orgCode) + let role = Role(organization: organization, + isGranted: rolesArray.contains(name)) + return role + } + return nil + } + public func getOrganization() -> Organization? { if let orgCodeClaim = getClaim(forKey: ClaimKey.organisationCode.rawValue), let orgCode = orgCodeClaim.value as? String { @@ -638,6 +775,33 @@ extension Auth { // Wrapper Methods + public func getBooleanFlag(code: String, defaultValue: Bool? = nil, options: ApiOptions? = nil) async throws -> Bool? { + if options?.forceApi == true { + let response = try await FeatureFlagsAPI.getFeatureFlags() + guard response.success else { + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Feature flags API returned success: false"]) + } + let flags = response.toFlagMap() + let flag = flags[code] + if let flag = flag, let boolValue = flag.value as? Bool { + return boolValue + } else { + return defaultValue + } + } else { + do { + if let value = try getFlag(code: code, defaultValue: defaultValue, flagType: .bool).value as? Bool { + return value + } else { + return defaultValue + } + } catch { + return defaultValue + } + } + } + + /// Get a boolean feature flag value (synchronous version, reads from token claims) public func getBooleanFlag(code: String, defaultValue: Bool? = nil) throws -> Bool { if let value = try getFlag(code: code, defaultValue: defaultValue, flagType: .bool).value as? Bool { return value @@ -650,6 +814,33 @@ extension Auth { } } + public func getStringFlag(code: String, defaultValue: String? = nil, options: ApiOptions? = nil) async throws -> String? { + if options?.forceApi == true { + let response = try await FeatureFlagsAPI.getFeatureFlags() + guard response.success else { + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Feature flags API returned success: false"]) + } + let flags = response.toFlagMap() + let flag = flags[code] + if let flag = flag, let stringValue = flag.value as? String { + return stringValue + } else { + return defaultValue + } + } else { + do { + if let value = try getFlag(code: code, defaultValue: defaultValue, flagType: .string).value as? String { + return value + } else { + return defaultValue + } + } catch { + return defaultValue + } + } + } + + /// Get a string feature flag value (synchronous version, reads from token claims) public func getStringFlag(code: String, defaultValue: String? = nil) throws -> String { if let value = try getFlag(code: code, defaultValue: defaultValue, flagType: .string).value as? String { return value @@ -662,6 +853,35 @@ extension Auth { } } + public func getIntegerFlag(code: String, defaultValue: Int? = nil, options: ApiOptions? = nil) async throws -> Int? { + if options?.forceApi == true { + let response = try await FeatureFlagsAPI.getFeatureFlags() + guard response.success else { + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Feature flags API returned success: false"]) + } + let flags = response.toFlagMap() + let flag = flags[code] + if let flag = flag, let intValue = flag.value as? Int { + return intValue + } else if let flag = flag, let numberValue = flag.value as? NSNumber { + return numberValue.intValue + } else { + return defaultValue + } + } else { + do { + if let value = try getFlag(code: code, defaultValue: defaultValue, flagType: .int).value as? Int { + return value + } else { + return defaultValue + } + } catch { + return defaultValue + } + } + } + + /// Get an integer feature flag value (synchronous version, reads from token claims) public func getIntegerFlag(code: String, defaultValue: Int? = nil) throws -> Int { if let value = try getFlag(code: code, defaultValue: defaultValue, flagType: .int).value as? Int { return value @@ -674,16 +894,56 @@ extension Auth { } } + /// Get all feature flags for the authenticated user + /// - Parameter options: Optional API options. Use ApiOptions(forceApi: true) to fetch fresh data from API + /// - Returns: Map of flag codes to Flag objects + public func getAllFlags(options: ApiOptions? = nil) async throws -> [String: Flag] { + if options?.forceApi == true { + let response = try await FeatureFlagsAPI.getFeatureFlags() + guard response.success else { + throw NSError(domain: "KindeSDK", code: -1, userInfo: [NSLocalizedDescriptionKey: "Feature flags API returned success: false"]) + } + return response.toFlagMap() + } else { + guard let featureFlagsClaim = getClaim(forKey: ClaimKey.featureFlags.rawValue), + let featureFlags = featureFlagsClaim.value as? [String: Any] else { + return [:] + } + + var flagMap: [String: Flag] = [:] + for (code, flagData) in featureFlags { + if let flagDict = flagData as? [String: Any], + let valueTypeLetter = flagDict["t"] as? String, + let flagType = Flag.ValueType(rawValue: valueTypeLetter), + let value = flagDict["v"] { + flagMap[code] = Flag(code: code, type: flagType, value: value, isDefault: false) + } + } + return flagMap + } + } + // Internal private func getFlagInternal(code: String, defaultValue: Any?, flagType: Flag.ValueType?) throws -> Flag { + // If no feature_flags claim exists, check if default value is provided guard let featureFlagsClaim = getClaim(forKey: ClaimKey.featureFlags.rawValue) else { - throw FlagError.unknownError + if let defaultValue = defaultValue { + // Default value provided - return it even if claim doesn't exist + return Flag(code: code, type: nil, value: defaultValue, isDefault: true) + } else { + throw FlagError.unknownError + } } guard let featureFlags = featureFlagsClaim.value as? [String : Any] else { - throw FlagError.unknownError + // Claim exists but is not a dictionary - check for default value + if let defaultValue = defaultValue { + return Flag(code: code, type: nil, value: defaultValue, isDefault: true) + } else { + throw FlagError.unknownError + } } if let flagData = featureFlags[code] as? [String: Any], @@ -901,6 +1161,34 @@ public struct Permissions: Codable { } } +/// Represents a role with organization context +public struct Role: Codable { + /// The organization this role belongs to + public let organization: Organization + + /// Whether the role is granted + public let isGranted: Bool + + public init(organization: Organization, isGranted: Bool) { + self.organization = organization + self.isGranted = isGranted + } +} + +/// Collection of roles +public struct Roles: Codable { + /// The organization these roles belong to + public let organization: Organization + + /// List of role names + public let roles: [String] + + public init(organization: Organization, roles: [String]) { + self.organization = organization + self.roles = roles + } +} + /// Collection of user organizations public struct UserOrganizations: Codable { /// List of organization codes @@ -934,6 +1222,29 @@ public class ClaimsService { public func getPermission(name: String) -> Bool { return auth.getPermission(name: name) != nil } + + /// Get all roles for the current user + /// - Returns: Roles if found, nil otherwise + public func getRoles() -> Roles? { + return auth.getRoles() + } + + /// Check if user has a specific role + /// - Parameter name: The role name to check + /// - Returns: Role if found, nil otherwise + public func getRole(name: String) -> Role? { + return auth.getRole(name: name) + } + + /// Get the raw value of a claim by key + /// - Parameter key: The claim key to retrieve + /// - Returns: The claim value if found, nil otherwise + public func getClaimValue(forKey key: String) -> Any? { + guard let claim = getClaim(forKey: key) else { + return nil + } + return claim.value.value + } } /// Service for managing user entitlements with type-safe API @@ -1187,6 +1498,88 @@ public class EntitlementsService { return fallbackValue } } + + /// Validate a user permission with hard check + /// - Parameters: + /// - permission: The permission name to validate + /// - fallbackAccess: Default access value if permission not found + /// - Returns: True if permission is granted, fallback value otherwise + public func validatePermission(permission: String, fallbackAccess: Bool) -> Bool { + return performHardCheck( + checkName: "permission:\(permission)", + validation: { auth.getPermission(name: permission)?.isGranted }, + fallbackValue: fallbackAccess + ) + } + + /// Validate a user role with hard check + /// - Parameters: + /// - role: The role name to validate + /// - fallbackAccess: Default access value if role not found + /// - Returns: True if user has the role, fallback value otherwise + public func validateRole(role: String, fallbackAccess: Bool) -> Bool { + return performHardCheck( + checkName: "role:\(role)", + validation: { + auth.getRole(name: role)?.isGranted + }, + fallbackValue: fallbackAccess + ) + } + + /// Validate a feature flag with hard check + /// - Parameters: + /// - flag: The feature flag code to validate + /// - fallbackEnabled: Default enabled value if flag not found + /// - Returns: True if feature flag is enabled, fallback value otherwise + public func validateFeatureFlag(flag: String, fallbackEnabled: Bool) -> Bool { + return performHardCheck( + checkName: "featureFlag:\(flag)", + validation: { + try? auth.getBooleanFlag(code: flag) + }, + fallbackValue: fallbackEnabled + ) + } + + /// Validate an entitlement with hard check + /// - Parameters: + /// - entitlement: The entitlement key to validate + /// - fallbackValue: Default value if entitlement not found + /// - Returns: Entitlement value if found, fallback value otherwise + public func validateEntitlement(entitlement: String, fallbackValue: String) -> String { + return performHardCheck( + checkName: "entitlement:\(entitlement)", + validation: { + if let value = getEntitlement(featureKey: entitlement) { + return String(describing: value) + } + return nil + }, + fallbackValue: fallbackValue + ) + } + + /// Check if the user is authenticated + /// - Returns: True if user is authenticated, false otherwise + public func isUserAuthenticated() -> Bool { + return auth.isAuthenticated() + } + + /// Get user organization context + /// - Returns: Dictionary with organization information, or empty dictionary if not available + public func getUserOrganization() -> [String: Any] { + guard let org = auth.getOrganization() else { + return [:] + } + return ["code": org.code] + } + + /// Get user subscription tier + /// - Returns: Subscription tier string, defaults to "free" if not found + public func getUserSubscriptionTier() -> String { + return getStringEntitlement(featureKey: "subscription_tier", defaultValue: "free") + } } /// Service for managing feature flags with type-safe API @@ -1262,6 +1655,7 @@ public class FeatureFlagsService { extension Auth { private enum ClaimKey: String { case permissions = "permissions" + case roles = "roles" case organisationCode = "org_code" case organisationCodes = "org_codes" case featureFlags = "feature_flags" diff --git a/Sources/KindeSDK/Auth/Config.swift b/Sources/KindeSDK/Auth/Config.swift index 360ae69..444eabc 100644 --- a/Sources/KindeSDK/Auth/Config.swift +++ b/Sources/KindeSDK/Auth/Config.swift @@ -1,6 +1,6 @@ import Foundation -/// Configuration for the Kinde authentication service and Kinde Management API client +/// Configuration for the Kinde authentication service public struct Config: Decodable { let issuer: String let clientId: String diff --git a/Sources/KindeSDK/Models/FeatureFlagsResponse.swift b/Sources/KindeSDK/Models/FeatureFlagsResponse.swift new file mode 100644 index 0000000..96d339a --- /dev/null +++ b/Sources/KindeSDK/Models/FeatureFlagsResponse.swift @@ -0,0 +1,79 @@ +import Foundation + +/// Response from the feature flags API endpoint +public struct FeatureFlagsResponse: Codable { + /// The data wrapper containing feature flags + public let data: FeatureFlagsData? + /// Whether the API call was successful + public let success: Bool + + private enum CodingKeys: String, CodingKey { + case data, success + } + + /// Check if the response is valid + public func isValid() -> Bool { + return success && data != nil + } + + /// Convert the API response to a map of flag keys to Flag objects + /// - Returns: Map of flag keys to Flag objects. Invalid flags (null key/value or unknown type) are skipped. + /// Valid types: "Boolean", "String", "Integer" (case-sensitive) + public func toFlagMap() -> [String: Flag] { + guard success else { + return [:] + } + let flags = data?.featureFlags ?? [] + var flagMap: [String: Flag] = [:] + + for item in flags { + guard let key = item.key else { + continue + } + guard let value = item.value else { + continue + } + + let flagType: Flag.ValueType? + switch item.type { + case "Boolean": + flagType = .bool + case "String": + flagType = .string + case "Integer": + flagType = .int + default: + continue + } + + flagMap[key] = Flag(code: key, type: flagType, value: value.value, isDefault: false) + } + + return flagMap + } +} + +/// Data wrapper for feature flags +public struct FeatureFlagsData: Codable { + /// List of feature flags + public let featureFlags: [FeatureFlagItem] + + private enum CodingKeys: String, CodingKey { + case featureFlags = "feature_flags" + } +} + +/// Individual feature flag item +public struct FeatureFlagItem: Codable { + /// Feature flag ID + public let id: String? + /// Feature flag key/code + public let key: String? + /// Feature flag display name + public let name: String? + /// Feature flag type (Boolean, String, Integer) + public let type: String? + /// Feature flag value + public let value: AnyCodable? +} + diff --git a/Sources/KindeSDK/Models/PermissionsResponse.swift b/Sources/KindeSDK/Models/PermissionsResponse.swift new file mode 100644 index 0000000..bd7f4f8 --- /dev/null +++ b/Sources/KindeSDK/Models/PermissionsResponse.swift @@ -0,0 +1,52 @@ +import Foundation + +/// Response from the permissions API endpoint +public struct PermissionsResponse: Codable { + /// The data wrapper containing permissions + public let data: PermissionsData? + /// Whether the API call was successful + public let success: Bool + + private enum CodingKeys: String, CodingKey { + case data, success + } + + /// Check if the response is valid + public func isValid() -> Bool { + return success && data != nil + } + + /// Extract permission keys from the API response + /// - Returns: List of permission keys. Permissions with null keys are skipped. + public func getPermissionKeys() -> [String] { + guard success else { + return [] + } + let permissions = data?.permissions ?? [] + return permissions.compactMap { $0.key } + } +} + +/// Data wrapper for permissions +public struct PermissionsData: Codable { + /// Organization code + public let orgCode: String? + /// List of permissions + public let permissions: [PermissionItem] + + private enum CodingKeys: String, CodingKey { + case orgCode = "org_code" + case permissions + } +} + +/// Individual permission item +public struct PermissionItem: Codable { + /// Permission ID + public let id: String? + /// Permission key/name + public let key: String? + /// Permission display name + public let name: String? +} + diff --git a/Sources/KindeSDK/Models/RolesResponse.swift b/Sources/KindeSDK/Models/RolesResponse.swift new file mode 100644 index 0000000..17cbc60 --- /dev/null +++ b/Sources/KindeSDK/Models/RolesResponse.swift @@ -0,0 +1,52 @@ +import Foundation + +/// Response from the roles API endpoint +public struct RolesResponse: Codable { + /// The data wrapper containing roles + public let data: RolesData? + /// Whether the API call was successful + public let success: Bool + + private enum CodingKeys: String, CodingKey { + case data, success + } + + /// Check if the response is valid + public func isValid() -> Bool { + return success && data != nil + } + + /// Extract role keys from the API response + /// - Returns: List of role keys. Roles with null keys are skipped. + public func getRoleKeys() -> [String] { + guard success else { + return [] + } + let roles = data?.roles ?? [] + return roles.compactMap { $0.key } + } +} + +/// Data wrapper for roles +public struct RolesData: Codable { + /// Organization code + public let orgCode: String? + /// List of roles + public let roles: [RoleItem] + + private enum CodingKeys: String, CodingKey { + case orgCode = "org_code" + case roles + } +} + +/// Individual role item +public struct RoleItem: Codable { + /// Role ID + public let id: String? + /// Role key/name + public let key: String? + /// Role display name + public let name: String? +} + diff --git a/Tests/AuthSpec.swift b/Tests/AuthSpec.swift index 9316859..e0373a2 100644 --- a/Tests/AuthSpec.swift +++ b/Tests/AuthSpec.swift @@ -6,13 +6,15 @@ import Foundation class AuthSpec: QuickSpec { override class func spec() { describe("Auth") { - it("is unauthorised after initialisation") { + beforeEach { KindeSDKAPI.configure() + } + + it("is unauthorised after initialisation") { expect(KindeSDKAPI.auth.isAuthenticated()).to(beFalse()) } it("check helper functions") { - KindeSDKAPI.configure() let auth: Auth = KindeSDKAPI.auth // Test authentication state @@ -28,6 +30,12 @@ class AuthSpec: QuickSpec { let permissions = auth.getPermissions() expect(permissions).to(beNil()) + let roles = auth.getRoles() + expect(roles).to(beNil()) + + let role = auth.getRole(name: "test_role") + expect(role).to(beNil()) + let organization = auth.getOrganization() expect(organization).to(beNil()) @@ -37,15 +45,15 @@ class AuthSpec: QuickSpec { // Feature Flags let testFlagCode = "#__testFlagCode__#" - let flagNotExistGetDefaultValue = try? auth.getFlag(code: testFlagCode, defaultValue: testFlagCode).value.value as? String + let flagNotExistGetDefaultValue = try? auth.getFlag(code: testFlagCode, defaultValue: testFlagCode).value as? String expect(flagNotExistGetDefaultValue).to(equal(testFlagCode)) expect( try auth.getFlag(code: testFlagCode) ) - .to( throwError(FlagError.notFound) ) + .to( throwError(FlagError.unknownError) ) let flagBoolNotExistGetNil = try? auth.getBooleanFlag(code: testFlagCode) expect(flagBoolNotExistGetNil).to(beNil()) expect( try auth.getBooleanFlag(code: testFlagCode) ) - .to( throwError(FlagError.notFound ) ) + .to( throwError(FlagError.unknownError) ) let flagBoolNotExistGetDefaultValue = try? auth.getBooleanFlag(code: testFlagCode, defaultValue: true) expect(flagBoolNotExistGetDefaultValue).to(equal(true)) @@ -55,7 +63,7 @@ class AuthSpec: QuickSpec { let flagStringNotExistGetNil = try? auth.getStringFlag(code: testFlagCode) expect(flagStringNotExistGetNil).to(beNil()) expect( try auth.getStringFlag(code: testFlagCode) ) - .to( throwError(FlagError.notFound) ) + .to( throwError(FlagError.unknownError) ) let flagStringNotExistGetDefaultValue = try? auth.getStringFlag(code: testFlagCode, defaultValue: testFlagCode) expect(flagStringNotExistGetDefaultValue).to(equal(testFlagCode)) @@ -65,7 +73,7 @@ class AuthSpec: QuickSpec { let flagIntNotExistGetNil = try? auth.getIntegerFlag(code: testFlagCode) expect(flagIntNotExistGetNil).to(beNil()) expect( try auth.getIntegerFlag(code: testFlagCode) ) - .to( throwError(FlagError.notFound) ) + .to( throwError(FlagError.unknownError) ) let flagIntNotExistGetDefaultValue = try? auth.getIntegerFlag(code: testFlagCode, defaultValue: 1) expect(flagIntNotExistGetDefaultValue).to(equal(1)) @@ -77,13 +85,117 @@ class AuthSpec: QuickSpec { let auth: Auth = KindeSDKAPI.auth // Test logout when not authenticated (should still work) - Task { - let result = await auth.logout() - // Logout should return true even when not authenticated - expect(result).to(equal(true)) - expect(auth.isAuthenticated()).to(beFalse()) + waitUntil(timeout: .seconds(5)) { done in + Task { + let result = await auth.logout() + // Logout should return true even when not authenticated + expect(result).to(equal(true)) + expect(auth.isAuthenticated()).to(beFalse()) + done() + } + } + } + + describe("ApiOptions") { + it("has default forceApi value of false") { + let options = ApiOptions() + expect(options.forceApi).to(beFalse()) + } + + it("can be initialized with forceApi true") { + let options = ApiOptions(forceApi: true) + expect(options.forceApi).to(beTrue()) + } + + it("can be initialized with forceApi false") { + let options = ApiOptions(forceApi: false) + expect(options.forceApi).to(beFalse()) } } + + describe("getAllFlags") { + it("returns empty dictionary when not authenticated and forceApi is false") { + let auth: Auth = KindeSDKAPI.auth + + waitUntil(timeout: .seconds(5)) { done in + Task { + do { + let flags = try await auth.getAllFlags() + expect(flags).to(beEmpty()) + done() + } catch { + // If it throws, that's also acceptable when not authenticated + done() + } + } + } + } + + it("returns empty dictionary when not authenticated and forceApi is true") { + let auth: Auth = KindeSDKAPI.auth + let options = ApiOptions(forceApi: true) + + waitUntil(timeout: .seconds(5)) { done in + Task { + do { + let flags = try await auth.getAllFlags(options: options) + // Should fail when not authenticated + expect(flags).to(beEmpty()) + done() + } catch { + // Expected to fail when not authenticated + expect(error).toNot(beNil()) + done() + } + } + } + } + } + + describe("async methods with forceApi") { + it("getPermissions with forceApi throws when not authenticated") { + let auth: Auth = KindeSDKAPI.auth + let options = ApiOptions(forceApi: true) + + waitUntil(timeout: .seconds(5)) { done in + Task { + do { + _ = try await auth.getPermissions(options: options) + // Should not reach here + expect(true).to(beFalse()) + done() + } catch { + // Expected to fail when not authenticated + expect(error).toNot(beNil()) + done() + } + } + } + } + + pending("Additional forceApi tests require authenticated user or mocked API responses") {} + } + + describe("async methods without forceApi (token claims)") { + it("getBooleanFlag without forceApi returns default value when flag doesn't exist") { + let auth: Auth = KindeSDKAPI.auth + waitUntil(timeout: .seconds(5)) { done in + Task { + do { + let result = try await auth.getBooleanFlag(code: "non_existent_flag", defaultValue: true) + expect(result).to(equal(true)) + done() + } catch { + // Should not throw when default value is provided + expect(true).to(beFalse()) + done() + } + } + } + } + + pending("Additional async tests require authenticated user or mocked tokens") {} + } } } } diff --git a/Tests/EntitlementsSpec.swift b/Tests/EntitlementsSpec.swift index a2a4c38..e64e23a 100644 --- a/Tests/EntitlementsSpec.swift +++ b/Tests/EntitlementsSpec.swift @@ -39,7 +39,7 @@ class EntitlementsSpec: QuickSpec { it("returns entitlement value when feature exists") { // This would require a mock token with specific entitlements // TODO: Add proper test fixtures with mocked tokens - pending("Requires mock token with entitlements claim") + pending("Requires mock token with entitlements claim") {} } } @@ -261,7 +261,7 @@ class EntitlementsSpec: QuickSpec { it("returns claim value when claim exists") { // This would require a mock token with specific claims // TODO: Add proper test fixtures with mocked tokens - pending("Requires mock token with claims") + pending("Requires mock token with claims") {} } } @@ -276,6 +276,34 @@ class EntitlementsSpec: QuickSpec { expect(result).to(beAKindOf(Any.self)) } } + + describe("getRoles") { + it("returns nil for non-existent roles when not authenticated") { + let result = claimsService.getRoles() + expect(result).to(beNil()) + } + + it("returns Roles type when roles exist") { + // This would require a mock token with roles claim + // For now, we test the structure and behavior + let result = claimsService.getRoles() + expect(result).to(beNil()) // Will be nil when not authenticated + } + } + + describe("getRole") { + it("returns nil for non-existent role when not authenticated") { + let result = claimsService.getRole(name: "non_existent_role") + expect(result).to(beNil()) + } + + it("returns Role type when role exists") { + // This would require a mock token with roles claim + // For now, we test the structure and behavior + let result = claimsService.getRole(name: "test_role") + expect(result).to(beNil()) // Will be nil when not authenticated + } + } } describe("EntitlementsService HTTP API") { diff --git a/kinde-auth.json b/kinde-auth.json index 610647a..4da93db 100644 --- a/kinde-auth.json +++ b/kinde-auth.json @@ -1,7 +1,7 @@ { - "issuer": "https://{your-business}.kinde.com", - "clientId": "{your-client-id}", - "redirectUri": "{your-url-scheme}://kinde_callback", - "postLogoutRedirectUri": "{your-url-scheme}://kinde_logoutcallback", - "scope": "offline openid email profile", + "issuer": "https://test.kinde.com", + "clientId": "test-client-id", + "redirectUri": "testapp://kinde_callback", + "postLogoutRedirectUri": "testapp://kinde_logoutcallback", + "scope": "offline openid email profile" } From b75d2c6e969dd2385c700bf3800370fbeed248f0 Mon Sep 17 00:00:00 2001 From: brandtkruger Date: Sun, 30 Nov 2025 19:44:49 +0200 Subject: [PATCH 6/8] fix coderabbit --- Tests/EntitlementsSpec.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Tests/EntitlementsSpec.swift b/Tests/EntitlementsSpec.swift index e64e23a..6dc2fb4 100644 --- a/Tests/EntitlementsSpec.swift +++ b/Tests/EntitlementsSpec.swift @@ -219,25 +219,26 @@ class EntitlementsSpec: QuickSpec { describe("isFeatureEnabled") { it("returns false for non-existent feature flag") { - let result = featureFlagsService.isFeatureEnabled(flag: "non_existent_flag") + let result = featureFlagsService.isFeatureEnabled(code: "non_existent_flag") expect(result).to(equal(false)) } it("returns default value for non-existent feature flag") { - let result = featureFlagsService.isFeatureEnabled(flag: "non_existent_flag", defaultValue: true) + let result = featureFlagsService.isFeatureEnabled(code: "non_existent_flag", defaultValue: true) expect(result).to(equal(true)) } } describe("getFeatureFlag") { - it("returns default value for non-existent feature flag") { - let result = featureFlagsService.getFeatureFlag(flag: "non_existent_flag", defaultValue: "default") - expect(result).to(equal("default")) + it("returns nil for non-existent feature flag") { + let result = featureFlagsService.getFeatureFlag(code: "non_existent_flag") + expect(result).to(beNil()) } - it("handles type conversion correctly") { - let result = featureFlagsService.getFeatureFlag(flag: "test_flag", defaultValue: 42) - expect(result).to(beAKindOf(Int.self)) + it("returns Any? type for feature flag") { + let result = featureFlagsService.getFeatureFlag(code: "test_flag") + // Result will be nil when flag doesn't exist + expect(result).to(beNil()) } } } From c206208a403dc35e2798011113df0e20fbf95feb Mon Sep 17 00:00:00 2001 From: brandtkruger Date: Wed, 3 Dec 2025 20:08:19 +0200 Subject: [PATCH 7/8] Remove Package.resolved references from Xcode project Addresses review comments from victoreronmosele: - Remove PBXBuildFile entry for Package.resolved - Remove PBXFileReference entry for Package.resolved - Remove from main group - Remove from Resources build phase Since Package.resolved is now in .gitignore, it should not be referenced in the Xcode project. --- KindeSDK.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/KindeSDK.xcodeproj/project.pbxproj b/KindeSDK.xcodeproj/project.pbxproj index 6616a67..6817d5b 100644 --- a/KindeSDK.xcodeproj/project.pbxproj +++ b/KindeSDK.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 3B1EBA7329F944E000444EEB /* KindeSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B1EBA6B29F944E000444EEB /* KindeSDK.framework */; settings = {ATTRIBUTES = (Required, ); }; }; 3B1EBAAF29F945DB00444EEB /* KindeSDK.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 3B1EBA8129F945DA00444EEB /* KindeSDK.podspec */; }; - 3B1EBAD329F945DB00444EEB /* Package.resolved in Resources */ = {isa = PBXBuildFile; fileRef = 3B1EBAAC29F945DA00444EEB /* Package.resolved */; }; 3B1EBAD429F945DB00444EEB /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 3B1EBAAD29F945DA00444EEB /* LICENSE */; }; 3B41B1B22A0E5F8500EFD9E4 /* AuthSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1AD2A0E5F8500EFD9E4 /* AuthSpec.swift */; }; 3B41B1B32A0E5F8500EFD9E4 /* StringJWTTokenDecodeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B41B1AE2A0E5F8500EFD9E4 /* StringJWTTokenDecodeSpec.swift */; }; @@ -79,7 +78,6 @@ 3B1EBA6B29F944E000444EEB /* KindeSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KindeSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B1EBA7229F944E000444EEB /* KindeSDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KindeSDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B1EBA8129F945DA00444EEB /* KindeSDK.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = KindeSDK.podspec; sourceTree = ""; }; - 3B1EBAAC29F945DA00444EEB /* Package.resolved */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Package.resolved; sourceTree = ""; }; 3B1EBAAD29F945DA00444EEB /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 3B41B1AD2A0E5F8500EFD9E4 /* AuthSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthSpec.swift; sourceTree = ""; }; 3B41B1AE2A0E5F8500EFD9E4 /* StringJWTTokenDecodeSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringJWTTokenDecodeSpec.swift; sourceTree = ""; }; @@ -162,7 +160,6 @@ 3BA67F842A0A5FCE00399C6A /* kinde-auth.json */, 3B1EBA8129F945DA00444EEB /* KindeSDK.podspec */, 3B1EBAAD29F945DA00444EEB /* LICENSE */, - 3B1EBAAC29F945DA00444EEB /* Package.resolved */, 3B1EBA4F29F943B200444EEB /* Products */, 3B1EBAF529F947DA00444EEB /* Frameworks */, ); @@ -403,7 +400,6 @@ 3B41B1DA2A0E5F9100EFD9E4 /* OAuthAPI.md in Resources */, 3B41B1DB2A0E5F9100EFD9E4 /* UserProfile.md in Resources */, 3B1EBAAF29F945DB00444EEB /* KindeSDK.podspec in Resources */, - 3B1EBAD329F945DB00444EEB /* Package.resolved in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 45b621499c31964dac0ef938e14da961c7c6becc Mon Sep 17 00:00:00 2001 From: brandtkruger Date: Wed, 3 Dec 2025 20:16:41 +0200 Subject: [PATCH 8/8] Remove Claims.swift and MobileEntitlements.swift references These files don't exist in the project and were causing build failures. Removed all references from: - PBXBuildFile section - PBXFileReference section - Sources build phase Addresses review comment from victoreronmosele. --- KindeSDK.xcodeproj/project.pbxproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/KindeSDK.xcodeproj/project.pbxproj b/KindeSDK.xcodeproj/project.pbxproj index 6817d5b..2c70c91 100644 --- a/KindeSDK.xcodeproj/project.pbxproj +++ b/KindeSDK.xcodeproj/project.pbxproj @@ -60,8 +60,6 @@ D773EAD42A897ED30049DDEE /* FlagError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773EAD32A897ED30049DDEE /* FlagError.swift */; }; D7FD1F682A8D59E20088802F /* SDKVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD1F672A8D59E20088802F /* SDKVersion.swift */; }; D7FD1F6E2A8D5D390088802F /* PList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD1F6D2A8D5D390088802F /* PList.swift */; }; - 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7868368F2B9D480CA62B8169 /* Claims.swift */; }; - 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67687A29ECEE460982748C70 /* MobileEntitlements.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -123,8 +121,6 @@ D7FD1F672A8D59E20088802F /* SDKVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKVersion.swift; sourceTree = ""; }; D7FD1F6C2A8D5CEC0088802F /* KindeSDK-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "KindeSDK-Info.plist"; sourceTree = ""; }; D7FD1F6D2A8D5D390088802F /* PList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PList.swift; sourceTree = ""; }; - 7868368F2B9D480CA62B8169 /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; - 67687A29ECEE460982748C70 /* MobileEntitlements.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MobileEntitlements.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -452,8 +448,6 @@ 3B41B1EF2A0E5F9100EFD9E4 /* JSONDataEncoding.swift in Sources */, D7FD1F6E2A8D5D390088802F /* PList.swift in Sources */, 3B41B1E22A0E5F9100EFD9E4 /* Auth.swift in Sources */, - 1FF4050EE3A3442F8C44FC3B /* Claims.swift in Sources */, - 7323339292584BFAA1CE541F /* MobileEntitlements.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };