Skip to content

Commit

Permalink
Release/1.2.0 (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
borut-t authored Oct 23, 2023
2 parents c584c0c + 234b9f0 commit da8af7b
Show file tree
Hide file tree
Showing 15 changed files with 578 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PovioKitAuthLinkedIn"
BuildableName = "PovioKitAuthLinkedIn"
BlueprintName = "PovioKitAuthLinkedIn"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
Expand Down
14 changes: 12 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import PackageDescription
let package = Package(
name: "PovioKitAuth",
platforms: [
.iOS(.v13)
.iOS(.v13),
.macOS(.v13)
],
products: [
.library(name: "PovioKitAuthCore", targets: ["PovioKitAuthCore"]),
.library(name: "PovioKitAuthApple", targets: ["PovioKitAuthApple"]),
.library(name: "PovioKitAuthGoogle", targets: ["PovioKitAuthGoogle"]),
.library(name: "PovioKitAuthFacebook", targets: ["PovioKitAuthFacebook"])
.library(name: "PovioKitAuthFacebook", targets: ["PovioKitAuthFacebook"]),
.library(name: "PovioKitAuthLinkedIn", targets: ["PovioKitAuthLinkedIn"])
],
dependencies: [
.package(url: "https://github.com/poviolabs/PovioKit", .upToNextMajor(from: "3.0.0")),
Expand Down Expand Up @@ -51,6 +53,14 @@ let package = Package(
],
path: "Sources/Facebook"
),
.target(
name: "PovioKitAuthLinkedIn",
dependencies: [
"PovioKitAuthCore",
.product(name: "PovioKitNetworking", package: "PovioKit")
],
path: "Sources/LinkedIn"
),
.testTarget(
name: "Tests",
dependencies: [
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@

## Packages

| [Core](Resources/Core) | [Apple](Resources/Apple) | [Google](Resources/Google) | [Facebook](Resources/Facebook) |
| :-: | :-: | :-: | :-: |
| [Core](Resources/Core) | [Apple](Resources/Apple) | [Google](Resources/Google) | [Facebook](Resources/Facebook) | [LinkedIn](Resources/LinkedIn) |
| :-: | :-: | :-: | :-: | :-: |

## Installation

Expand All @@ -38,6 +38,7 @@
- *PovioKitAuthApple* (Apple auth components)
- *PovioKitAuthGoogle* (Google auth components)
- *PovioKitAuthFacebook* (Facebook auth components)
- *PovioKitAuthLinkedIn* (LinkedIn auth components)
- Select "Add Package" again and you are done.

### Migration
Expand Down
32 changes: 32 additions & 0 deletions Resources/LinkedIn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# LinkedInAuthenticator

Auth provider for social login with LinkedIn.

## Setup
Please read [official documentation](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin%2Fcontext&tabs=HTTPS1) from LinkedIn for the details about the authorization.

## Usage

```swift
// present login screen
body
.sheet(isPresented: $openLinkedInWebView) {
LinkedInWebView(with: linkedInConfig) { data in
Task { await viewModel.signInWithLinkedIn(authCode: data.code) }
} onFailure: {
viewModel.error = .general
}
}

// handle response from webView
let authResponse = try await auth.signIn(authCode: authCode, configuration: linkedInConfig)

// get authentication status
let state = authenticator.isAuthenticated

// signOut user
authenticator.signOut() // all provider data regarding the use auth is cleared at this point

// handle url
authenticator.canOpenUrl(_: application: options:) // call this from `application:openURL:options:` in UIApplicationDelegate
```
Binary file modified Sources/.DS_Store
Binary file not shown.
5 changes: 3 additions & 2 deletions Sources/Apple/AppleAuthenticator+Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ public extension AppleAuthenticator {
struct Response {
public let userId: String
public let token: String
public let authCode: String
public let name: String?
public let email: Email?
public let expiresAt: Date?
public let email: Email
public let expiresAt: Date
}
}

Expand Down
21 changes: 19 additions & 2 deletions Sources/Apple/AppleAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ extension AppleAuthenticator: ASAuthorizationControllerDelegate {
public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
switch authorization.credential {
case let credential as ASAuthorizationAppleIDCredential:
guard let identityToken = credential.identityToken,
guard let authCodeData = credential.authorizationCode,
let authCode = String(data: authCodeData, encoding: .utf8),
let identityToken = credential.identityToken,
let identityTokenString = String(data: identityToken, encoding: .utf8) else {
rejectSignIn(with: .invalidIdentityToken)
return
Expand All @@ -108,11 +110,24 @@ extension AppleAuthenticator: ASAuthorizationControllerDelegate {
return .init(address: $0, isPrivate: isEmailPrivate, isVerified: isEmailVerified)
}

// do not continue if `email` is missing
guard let email else {
rejectSignIn(with: .missingEmail)
return
}

// do not continue if `expiresAt` is missing
guard let expiresAt = jwt?.expiresAt else {
rejectSignIn(with: .missingExpiration)
return
}

let response = Response(userId: credential.user,
token: identityTokenString,
authCode: authCode,
name: credential.displayName,
email: email,
expiresAt: jwt?.expiresAt)
expiresAt: expiresAt)

processingPromise?.resolve(with: response)
case _:
Expand All @@ -139,6 +154,8 @@ public extension AppleAuthenticator {
case invalidIdentityToken
case unhandledAuthorization
case credentialsRevoked
case missingExpiration
case missingEmail
}
}

Expand Down
23 changes: 23 additions & 0 deletions Sources/LinkedIn/API/EndpointEncodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// EndpointEncodable.swift
// PovioKitAuth
//
// Created by Borut Tomazin on 04/09/2023.
// Copyright © 2023 Povio Inc. All rights reserved.
//

import Foundation
import PovioKitNetworking

protocol EndpointEncodable: URLConvertible {
typealias Path = String

var path: Path { get }
var url: String { get }
}

extension EndpointEncodable {
func asURL() throws -> URL {
.init(stringLiteral: url)
}
}
38 changes: 38 additions & 0 deletions Sources/LinkedIn/API/LinkedInAPI+Endpoints.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// LinkedInAPI+Endpoints.swift
// PovioKitAuth
//
// Created by Borut Tomazin on 04/09/2023.
// Copyright © 2023 Povio Inc. All rights reserved.
//

import Foundation
import PovioKitNetworking

extension LinkedInAPI {
enum Endpoints: EndpointEncodable {
case accessToken
case profile
case email

var path: Path {
switch self {
case .accessToken:
return "accessToken"
case .profile:
return "me"
case .email:
return "emailAddress?q=members&projection=(elements*(handle~))"
}
}

var url: String {
switch self {
case .accessToken:
return "https://www.linkedin.com/oauth/v2/\(path)"
case .profile, .email:
return "https://api.linkedin.com/v2/\(path)"
}
}
}
}
61 changes: 61 additions & 0 deletions Sources/LinkedIn/API/LinkedInAPI+Models.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// LinkedInAPI+Models.swift
// PovioKitAuth
//
// Created by Borut Tomazin on 04/09/2023.
// Copyright © 2023 Povio Inc. All rights reserved.
//

import Foundation

public extension LinkedInAPI {
struct LinkedInAuthRequest: Encodable {
let grantType: String = "authorization_code"
let code: String
let redirectUri: String
let clientId: String
let clientSecret: String

public init(code: String, redirectUri: String, clientId: String, clientSecret: String) {
self.code = code
self.redirectUri = redirectUri
self.clientId = clientId
self.clientSecret = clientSecret
}
}

struct LinkedInAuthResponse: Decodable {
public let accessToken: String
public let expiresIn: Date
}

struct LinkedInProfileRequest: Encodable {
let token: String

public init(token: String) {
self.token = token
}
}

struct LinkedInProfileResponse: Decodable {
public let id: String
public let localizedFirstName: String
public let localizedLastName: String
}

struct LinkedInEmailResponse: Decodable {
public let elements: [LinkedInEmailHandleResponse]
}

struct LinkedInEmailHandleResponse: Decodable {
public let handle: LinkedInEmailValueResponse

enum CodingKeys: String, CodingKey {
case handle = "handle~"
}
}

struct LinkedInEmailValueResponse: Decodable {
public let emailAddress: String
}
}
79 changes: 79 additions & 0 deletions Sources/LinkedIn/API/LinkedInAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// LinkedInAPI.swift
// PovioKitAuth
//
// Created by Borut Tomazin on 04/09/2023.
// Copyright © 2023 Povio Inc. All rights reserved.
//

import Foundation
import PovioKitNetworking

public final class LinkedInAPI {
private let client: AlamofireNetworkClient

public init(client: AlamofireNetworkClient = .init()) {
self.client = client
}
}

public extension LinkedInAPI {
func login(with request: LinkedInAuthRequest) async throws -> LinkedInAuthResponse {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let secondsRemaining = try container.decode(Int.self)
return Date().addingTimeInterval(TimeInterval(secondsRemaining))
}

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase

return try await client
.request(
method: .post,
endpoint: Endpoints.accessToken,
encode: request,
parameterEncoder: .urlEncoder(encoder: encoder)
)
.validate()
.decode(LinkedInAuthResponse.self, decoder: decoder)
.asAsync
}

func loadProfile(with request: LinkedInProfileRequest) async throws -> LinkedInProfileResponse {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601

return try await client
.request(
method: .get,
endpoint: Endpoints.profile,
headers: ["Authorization": "Bearer \(request.token)"]
)
.validate()
.decode(LinkedInProfileResponse.self, decoder: decoder)
.asAsync
}

func loadEmail(with request: LinkedInProfileRequest) async throws -> LinkedInEmailValueResponse {
return try await client
.request(
method: .get,
endpoint: Endpoints.email,
headers: ["Authorization": "Bearer \(request.token)"])
.validate()
.decode(LinkedInEmailResponse.self)
.compactMap { $0.elements.first?.handle }
.asAsync
}
}

// MARK: - Error
public extension LinkedInAPI {
enum Error: Swift.Error {
case missingParameters
}
}
Loading

0 comments on commit da8af7b

Please sign in to comment.