Skip to content

Commit

Permalink
Add user custom attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
itaihanski committed Jan 4, 2024
1 parent aeab474 commit c381bd6
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 3 deletions.
54 changes: 54 additions & 0 deletions src/internal/http/DescopeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,60 @@ class DescopeClient: HTTPClient {
var givenName: String?
var middleName: String?
var familyName: String?
var customAttributes: [String: Any] = [:]

enum CodingKeys: CodingKey {
case userId
case loginIds
case createdTime
case name
case picture
case email
case verifiedEmail
case phone
case verifiedPhone
case givenName
case middleName
case familyName
case customAttributes
}

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
userId = try values.decode(String.self, forKey: .userId)
loginIds = try values.decode(Array<String>.self, forKey: .loginIds)
createdTime = try values.decode(Int.self, forKey: .createdTime)
if let value = try? values.decode(String?.self, forKey: .name) {
name = value
}
if let value = try? values.decode(String?.self, forKey: .picture) {
picture = value
}
if let value = try? values.decode(String?.self, forKey: .email) {
email = value
}
if let value = try? values.decode(Bool.self, forKey: .verifiedEmail) {
verifiedEmail = value
}
if let value = try? values.decode(String?.self, forKey: .phone) {
phone = value
}
if let value = try? values.decode(Bool?.self, forKey: .verifiedPhone) {
verifiedPhone = value
}
if let value = try? values.decode(String?.self, forKey: .givenName) {
givenName = value
}
if let value = try? values.decode(String?.self, forKey: .middleName) {
middleName = value
}
if let value = try? values.decode(String?.self, forKey: .familyName) {
familyName = value
}
if let value = try? values.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: .customAttributes) {
customAttributes = decodeJson(container: value)
}
}
}

struct MaskedAddress: JSONResponse {
Expand Down
116 changes: 114 additions & 2 deletions src/internal/others/Internal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extension Data {
}
self.init(base64Encoded: str, options: options)
}

func base64URLEncodedString(options: Base64EncodingOptions = []) -> String {
return base64EncodedString(options: options)
.replacingOccurrences(of: "+", with: "-")
Expand Down Expand Up @@ -73,9 +73,121 @@ class AuthorizationDelegate: NSObject, ASAuthorizationControllerDelegate {
completion?(.success(authorization))
completion = nil
}

func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
completion?(.failure(error))
completion = nil
}
}

// JSON

struct JSONCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?

init(stringValue: String) {
self.stringValue = stringValue
}

init(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}

func decodeJson(container: KeyedDecodingContainer<JSONCodingKeys>) -> [String: Any] {
var decoded: [String: Any] = [:]
for key in container.allKeys {
if let boolValue = try? container.decode(Bool.self, forKey: key) {
decoded[key.stringValue] = boolValue
} else if let intValue = try? container.decode(Int.self, forKey: key) {
decoded[key.stringValue] = intValue
} else if let doubleValue = try? container.decode(Double.self, forKey: key) {
decoded[key.stringValue] = doubleValue
} else if let stringValue = try? container.decode(String.self, forKey: key) {
decoded[key.stringValue] = stringValue
} else if let nestedContainer = try? container.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) {
decoded[key.stringValue] = decodeJson(container: nestedContainer)
} else if var nestedUnkeyedContainer = try? container.nestedUnkeyedContainer(forKey: key) {
decoded[key.stringValue] = decodeJson(unkeydContainer: &nestedUnkeyedContainer)
}
}
return decoded
}


func decodeJson(unkeydContainer: inout UnkeyedDecodingContainer) -> [Any] {
var decoded: [Any] = []
while unkeydContainer.isAtEnd == false {
if let value = try? unkeydContainer.decode(Bool.self) {
decoded.append(value)
} else if let value = try? unkeydContainer.decode(Int.self) {
decoded.append(value)
} else if let value = try? unkeydContainer.decode(Double.self) {
decoded.append(value)
} else if let value = try? unkeydContainer.decode(String.self) {
decoded.append(value)
} else if let _ = try? unkeydContainer.decode(String?.self) {
continue // Skip over `null` values
} else if let nestedContainer = try? unkeydContainer.nestedContainer(keyedBy: JSONCodingKeys.self) {
decoded.append(decodeJson(container: nestedContainer))
} else if var nestedUnkeyedContainer = try? unkeydContainer.nestedUnkeyedContainer() {
decoded.append(decodeJson(unkeydContainer: &nestedUnkeyedContainer))
}
}
return decoded
}

extension Dictionary<String, Any> {
func encodeJson(container: inout KeyedEncodingContainer<JSONCodingKeys>) throws {
try forEach({ (key, value) in
let encodingKey = JSONCodingKeys(stringValue: key)
switch value {
case let value as Bool:
try container.encode(value, forKey: encodingKey)
case let value as Int:
try container.encode(value, forKey: encodingKey)
case let value as Double:
try container.encode(value, forKey: encodingKey)
case let value as String:
try container.encode(value, forKey: encodingKey)
case let value as String?:
try container.encode(value, forKey: encodingKey)
case let value as [String: Any]:
var nestedContainer = container.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: encodingKey)
try value.encodeJson(container: &nestedContainer)
case let value as [Any]:
var nestedUnkeyedContainer = container.nestedUnkeyedContainer(forKey: encodingKey)
try value.encodeJson(container: &nestedUnkeyedContainer)
default:
throw DescopeError.encodeError.with(message: "Invalid JSON value in dict: \(key): \(value)")
}
})
}
}

extension Array<Any> {
func encodeJson(container: inout UnkeyedEncodingContainer) throws {
for value in self {
switch value {
case let value as Bool:
try container.encode(value)
case let value as Int:
try container.encode(value)
case let value as Double:
try container.encode(value)
case let value as String:
try container.encode(value)
case let value as [String: Any]:
var nestedContainer = container.nestedContainer(keyedBy: JSONCodingKeys.self)
try value.encodeJson(container: &nestedContainer)
case let value as [Any]:
var nestedUnkeyedContainer = container.nestedUnkeyedContainer()
try value.encodeJson(container: &nestedUnkeyedContainer)
default:
throw DescopeError.encodeError.with(message: "Invalid JSON value in array: \(value)")
}
}
}
}
1 change: 1 addition & 0 deletions src/internal/routes/Shared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ extension DescopeClient.UserResponse {
if let familyName, !familyName.isEmpty {
me.familyName = familyName
}
me.customAttributes = customAttributes
return me
}
}
Expand Down
75 changes: 74 additions & 1 deletion src/types/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ public struct DescopeUser: Codable, Equatable {
/// The user's family name.
public var familyName: String?

public init(userId: String, loginIds: [String], createdAt: Date, name: String? = nil, picture: URL? = nil, email: String? = nil, isVerifiedEmail: Bool = false, phone: String? = nil, isVerifiedPhone: Bool = false, givenName: String? = nil, middleName: String? = nil, familyName: String? = nil) {
/// A mapping of any custom attributes associated with this user.
/// User custom attributes are managed via the Descope console.
public var customAttributes: [String: Any]

public init(userId: String, loginIds: [String], createdAt: Date, name: String? = nil, picture: URL? = nil, email: String? = nil, isVerifiedEmail: Bool = false, phone: String? = nil, isVerifiedPhone: Bool = false, givenName: String? = nil, middleName: String? = nil, familyName: String? = nil, customAttributes: [String: Any] = [:]) {
self.userId = userId
self.loginIds = loginIds
self.createdAt = createdAt
Expand All @@ -93,6 +97,75 @@ public struct DescopeUser: Codable, Equatable {
self.givenName = givenName
self.middleName = middleName
self.familyName = familyName
self.customAttributes = customAttributes
}

enum CodingKeys: CodingKey {
case userId
case loginIds
case createdAt
case name
case picture
case email
case isVerifiedEmail
case phone
case isVerifiedPhone
case givenName
case middleName
case familyName
case customAttributes
}

public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
userId = try values.decode(String.self, forKey: .userId)
loginIds = try values.decode(Array<String>.self, forKey: .loginIds)
createdAt = try values.decode(Date.self, forKey: .createdAt)
name = try values.decode(String?.self, forKey: .name)
picture = try values.decode(URL?.self, forKey: .picture)
email = try values.decode(String?.self, forKey: .email)
isVerifiedEmail = try values.decode(Bool.self, forKey: .isVerifiedEmail)
phone = try values.decode(String?.self, forKey: .phone)
isVerifiedPhone = try values.decode(Bool.self, forKey: .isVerifiedPhone)
givenName = try values.decode(String?.self, forKey: .givenName)
middleName = try values.decode(String?.self, forKey: .middleName)
familyName = try values.decode(String?.self, forKey: .familyName)
let value = try values.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: .customAttributes)
customAttributes = decodeJson(container: value)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(userId, forKey: .userId)
try container.encode(loginIds, forKey: .loginIds)
try container.encode(createdAt, forKey: .createdAt)
try container.encode(name, forKey: .name)
try container.encode(picture, forKey: .picture)
try container.encode(email, forKey: .email)
try container.encode(isVerifiedEmail, forKey: .isVerifiedEmail)
try container.encode(phone, forKey: .phone)
try container.encode(isVerifiedPhone, forKey: .isVerifiedPhone)
try container.encode(givenName, forKey: .givenName)
try container.encode(middleName, forKey: .middleName)
try container.encode(familyName, forKey: .familyName)
var nestedContainer = container.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: .customAttributes)
try customAttributes.encodeJson(container: &nestedContainer)
}

public static func == (lhs: DescopeUser, rhs: DescopeUser) -> Bool {
return lhs.userId == rhs.userId &&
lhs.loginIds == rhs.loginIds &&
lhs.createdAt == rhs.createdAt &&
lhs.name == rhs.name &&
lhs.picture == rhs.picture &&
lhs.email == rhs.email &&
lhs.isVerifiedEmail == rhs.isVerifiedEmail &&
lhs.phone == rhs.phone &&
lhs.isVerifiedPhone == rhs.isVerifiedPhone &&
lhs.givenName == rhs.givenName &&
lhs.middleName == rhs.middleName &&
lhs.familyName == rhs.familyName
// lhs.customAttributes == rhs.customAttributes
}
}

Expand Down
93 changes: 93 additions & 0 deletions test/http/DescopeClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import XCTest
@testable import DescopeKit

class TestResponses: XCTestCase {
func testUserResponse() throws {
let jsonString = """
{
"userId": "userId",
"loginIds": ["loginId"],
"name": "name",
"picture": "picture",
"email": "email",
"verifiedEmail": true,
"phone": "phone",
"createdTime": 123,
"middleName": "middleName",
"familyName": "familyName",
"customAttributes": {
"a": "yes",
"b": true,
"c": 1,
"d": null,
"unnecessaryArray": [
"yes",
true,
null,
1,
{
"a": "yes",
"b": true,
"c": 1,
"d": null
}
]
}
}
"""
guard let data = jsonString.data(using: .utf8) else { return XCTFail("Couldn't get data from json string")}
guard let userResponse = try? JSONDecoder().decode(DescopeClient.UserResponse.self, from: data) else { return XCTFail("Couldn't decode") }
XCTAssertEqual("userId", userResponse.userId)
XCTAssertFalse(userResponse.verifiedPhone)
XCTAssertNil(userResponse.givenName)

// customAttributes
try checkDictionary(userResponse.customAttributes)

// customAttributes.unnecessaryArray
guard let array = userResponse.customAttributes["unnecessaryArray"] as? Array<Any> else { return XCTFail("Couldn't get custom attirubte value as array") }
XCTAssertEqual(4, array.count) // null value omitted
try checkArray(array)

// customAttributes.unnecessaryArray[3]
guard let dict = array[3] as? [String: Any] else { return XCTFail("Couldn't get custom attirubte value as array") }
try checkDictionary(dict)

// convert to DescopeUser and check again
let user = userResponse.convert()
XCTAssertEqual("userId", user.userId)
XCTAssertFalse(user.isVerifiedPhone)
XCTAssertNil(user.givenName)

// customAttributes
try checkDictionary(user.customAttributes)

// customAttributes.unnecessaryArray
guard let array = user.customAttributes["unnecessaryArray"] as? Array<Any> else { return XCTFail("Couldn't get custom attirubte value as array") }
XCTAssertEqual(4, array.count) // null value omitted
try checkArray(array)

// customAttributes.unnecessaryArray[3]
guard let dict = array[3] as? [String: Any] else { return XCTFail("Couldn't get custom attirubte value as array") }
try checkDictionary(dict)
}

func checkDictionary(_ dict: [String:Any]) throws {
guard let aValue = dict["a"] as? String else { return XCTFail("Couldn't get custom attirubte value as String") }
XCTAssertEqual("yes", aValue)
guard let bValue = dict["b"] as? Bool else { return XCTFail("Couldn't get custom attirubte value as Bool") }
XCTAssertTrue(bValue)
guard let cValue = dict["c"] as? Int else { return XCTFail("Couldn't get custom attirubte value as Int") }
XCTAssertEqual(1, cValue)
XCTAssertNil(dict["d"])
}

func checkArray(_ array: [Any]) throws {
guard let aValue = array[0] as? String else { return XCTFail("Couldn't get custom attirubte value as string") }
XCTAssertEqual("yes", aValue)
guard let bValue = array[1] as? Bool else { return XCTFail("Couldn't get custom attirubte value as bool") }
XCTAssertTrue(bValue)
guard let cValue = array[2] as? Int else { return XCTFail("Couldn't get custom attirubte value as int") }
XCTAssertEqual(1, cValue)
}
}
Loading

0 comments on commit c381bd6

Please sign in to comment.