Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tvOS Device Authorization Flow #26

Merged
merged 51 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
33c1f92
Initial setup of DeviceAuthorizationFlowClient
bleege Oct 31, 2023
ae376f6
Converting times to Int64 rather than TimeInterval
bleege Oct 31, 2023
35339c1
Initial device code endpoint buildout
bleege Nov 1, 2023
ae31a5c
DAF Client Port
bleege Nov 1, 2023
a69c5ff
DAF Get Token Function
bleege Nov 3, 2023
2f0d855
onError() and scheduleNextCheck()
bleege Nov 3, 2023
abdf824
TODO
bleege Nov 3, 2023
3b1096a
Check Authorization Build Out
bleege Nov 5, 2023
d228186
Error handling for Check Authorization
bleege Nov 6, 2023
62bc942
Cleaning up constants
bleege Nov 6, 2023
945d772
Lint
bleege Nov 6, 2023
eeecf33
Consolidate get Tokens API Call
bleege Nov 6, 2023
8fa6b02
Basic UX / Nav for DAF
bleege Nov 6, 2023
fdacc70
DAF UI
bleege Nov 6, 2023
a029156
Fixing DocC Comments
bleege Nov 6, 2023
b23ad31
Setting up DAF via Bottom Sheet
bleege Nov 7, 2023
2b5d330
Wiring up DAF UI with Functionality
bleege Nov 7, 2023
4ef77a5
Fixing Get Device Code URL Path
bleege Nov 7, 2023
ed71090
Doc fix
bleege Nov 7, 2023
4ee180e
Error support for DeviceCodeResponse
bleege Nov 8, 2023
3c76a7b
Refactoring Client API Networking
bleege Nov 8, 2023
1573f1f
Further Separating DeviceTokenResponse From OpenPassTokensResponse
bleege Nov 8, 2023
388e38d
Improve Typed Error Handling
bleege Nov 8, 2023
c412fbe
Persist Device Code After Generation
bleege Nov 8, 2023
2effbcc
Cleaning
bleege Nov 8, 2023
cfe24a1
Wiring up Dev App DAF View With DAF Functionality
bleege Nov 8, 2023
1c000b7
Verification URI Complete
bleege Nov 8, 2023
69b4c63
Update UI With OpenPassTokens On Complete
bleege Nov 8, 2023
664de0e
Web and TV Client Ids
bleege Nov 8, 2023
da86714
Removing TokensListView
bleege Nov 8, 2023
ec9a21f
tvOS Only
bleege Nov 9, 2023
f306c65
Clean Up
bleege Nov 9, 2023
257373c
Separating DeviceAuthorizationView and Removing Unnecessary Entitlement
bleege Nov 9, 2023
d06b7bb
testGetDeviceCode()
bleege Nov 12, 2023
9169211
testDeviceCodeError()
bleege Nov 12, 2023
72c462a
testGetTokenFromDeviceCode()
bleege Nov 12, 2023
48378f8
testGetTokenFromDeviceCodeError()
bleege Nov 12, 2023
ca3b516
Expired Token
bleege Nov 13, 2023
2690049
Fix comment
bleege Nov 16, 2023
7d5de9d
Installing Mockingbird
bleege Nov 18, 2023
50780d3
Ignore Mockingbird Meta Code
bleege Nov 18, 2023
3d404ee
Fixing Web Client Id
bleege Nov 18, 2023
3ad8324
Removing Mockingbird
bleege Nov 19, 2023
25e2905
Merge branch 'main' into bwl-TTDCP-4198-device-flow
dcaunt Mar 25, 2024
ad0bad5
Refactor Flow for async await
dcaunt Mar 28, 2024
c2cb077
Restore client ID
dcaunt Mar 28, 2024
9ca6921
Add manager test, restore client tests
dcaunt Mar 28, 2024
7655b64
Clean up comments
dcaunt Mar 28, 2024
389459c
Clean dev app, run tvOS tests on CI
dcaunt Apr 3, 2024
9baa6f4
Add note about Client IDs
dcaunt Apr 3, 2024
23d1741
Fix documentation for fetchDeviceCode and polling
dcaunt Apr 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
# this allows us to manually run this job
workflow_dispatch:

# https://github.com/actions/runner-images/?tab=readme-ov-file#available-images
jobs:

test-code-changes:
Expand All @@ -25,5 +26,8 @@ jobs:
- name: Build for tvOS
run: xcodebuild -scheme OpenPass -destination "generic/platform=tvOS"

- name: Run unit tests
- name: Run unit tests on iOS
run: xcodebuild test -scheme OpenPassTests -sdk iphonesimulator16.2 -destination "OS=16.2,name=iPhone 14"

- name: Run unit tests on tvOS
run: xcodebuild test -scheme OpenPassTests -sdk appletvsimulator16.1 -destination "OS=16.1,name=Apple TV"
22 changes: 14 additions & 8 deletions Development/OpenPassDevelopmentApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@
objects = {

/* Begin PBXBuildFile section */
BFEB6D9D2BB2F1E600A1CD75 /* DeviceAuthorizationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEB6D9C2BB2F1E600A1CD75 /* DeviceAuthorizationViewModel.swift */; };
E245BA5F293F9C150053DCB2 /* OpenPassTokensView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E245BA5E293F9C150053DCB2 /* OpenPassTokensView.swift */; };
E262B3DD2913FD1100C55A06 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E262B3DC2913FD1100C55A06 /* Localizable.strings */; };
E298111128F9DDC400A6A6E0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E298111028F9DDC400A6A6E0 /* AppDelegate.swift */; };
E298111A28F9DDC500A6A6E0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E298111928F9DDC500A6A6E0 /* Assets.xcassets */; };
E2A122E228FEEAD200EBB04F /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A122E128FEEAD200EBB04F /* RootView.swift */; };
E2A122E428FEEBA000EBB04F /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A122E328FEEBA000EBB04F /* RootViewModel.swift */; };
E2B0B625293F5F5700E89786 /* ErrorListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B0B624293F5F5700E89786 /* ErrorListView.swift */; };
E2B0B627293F60DF00E89786 /* TokensListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B0B626293F60DF00E89786 /* TokensListView.swift */; };
E2BFD14B2AFD1F780066EB8D /* DeviceAuthorizationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2BFD14A2AFD1F780066EB8D /* DeviceAuthorizationView.swift */; };
E2E9795A28F9E5AE00E46CE8 /* OpenPass in Frameworks */ = {isa = PBXBuildFile; productRef = E2E9795928F9E5AE00E46CE8 /* OpenPass */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
BFEB6D9C2BB2F1E600A1CD75 /* DeviceAuthorizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAuthorizationViewModel.swift; sourceTree = "<group>"; };
E245BA5E293F9C150053DCB2 /* OpenPassTokensView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenPassTokensView.swift; sourceTree = "<group>"; };
E262B3DC2913FD1100C55A06 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
E298110D28F9DDC400A6A6E0 /* OpenPassDevelopmentApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenPassDevelopmentApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand All @@ -29,7 +31,7 @@
E2A122E128FEEAD200EBB04F /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
E2A122E328FEEBA000EBB04F /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; };
E2B0B624293F5F5700E89786 /* ErrorListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorListView.swift; sourceTree = "<group>"; };
E2B0B626293F60DF00E89786 /* TokensListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokensListView.swift; sourceTree = "<group>"; };
E2BFD14A2AFD1F780066EB8D /* DeviceAuthorizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAuthorizationView.swift; sourceTree = "<group>"; };
E2E9795728F9E58900E46CE8 /* openpass-ios-sdk */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "openpass-ios-sdk"; path = ..; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -70,8 +72,9 @@
E2A122E128FEEAD200EBB04F /* RootView.swift */,
E2A122E328FEEBA000EBB04F /* RootViewModel.swift */,
E2B0B624293F5F5700E89786 /* ErrorListView.swift */,
E2B0B626293F60DF00E89786 /* TokensListView.swift */,
E245BA5E293F9C150053DCB2 /* OpenPassTokensView.swift */,
E2BFD14A2AFD1F780066EB8D /* DeviceAuthorizationView.swift */,
BFEB6D9C2BB2F1E600A1CD75 /* DeviceAuthorizationViewModel.swift */,
E298111928F9DDC500A6A6E0 /* Assets.xcassets */,
E298111B28F9DDC500A6A6E0 /* LaunchScreen.storyboard */,
E298111E28F9DDC500A6A6E0 /* Info.plist */,
Expand Down Expand Up @@ -190,8 +193,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E2BFD14B2AFD1F780066EB8D /* DeviceAuthorizationView.swift in Sources */,
E245BA5F293F9C150053DCB2 /* OpenPassTokensView.swift in Sources */,
E2B0B627293F60DF00E89786 /* TokensListView.swift in Sources */,
BFEB6D9D2BB2F1E600A1CD75 /* DeviceAuthorizationViewModel.swift in Sources */,
E298111128F9DDC400A6A6E0 /* AppDelegate.swift in Sources */,
E2A122E428FEEBA000EBB04F /* RootViewModel.swift in Sources */,
E2A122E228FEEAD200EBB04F /* RootView.swift in Sources */,
Expand Down Expand Up @@ -271,6 +275,7 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
TVOS_DEPLOYMENT_TARGET = 15.6;
};
name = Debug;
};
Expand Down Expand Up @@ -325,6 +330,7 @@
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = complete;
TVOS_DEPLOYMENT_TARGET = 15.6;
VALIDATE_PRODUCT = YES;
};
name = Release;
Expand All @@ -334,10 +340,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = OpenPassDevelopmentApp/OpenPassDevelopmentApp.entitlements;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 7HDA78784S;
DEVELOPMENT_TEAM = UW77J35X4D;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = OpenPassDevelopmentApp/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
Expand Down Expand Up @@ -366,10 +372,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = OpenPassDevelopmentApp/OpenPassDevelopmentApp.entitlements;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 7HDA78784S;
DEVELOPMENT_TEAM = UW77J35X4D;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = OpenPassDevelopmentApp/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
Expand Down
107 changes: 107 additions & 0 deletions Development/OpenPassDevelopmentApp/DeviceAuthorizationView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//
// DeviceAuthorizationView.swift
//
// MIT License
//
// Copyright (c) 2024 The Trade Desk (https://www.thetradedesk.com/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//

#if os(tvOS)
import OpenPass
import SwiftUI

struct DeviceAuthorizationView: View {

@ObservedObject
private var viewModel = DeviceAuthorizationViewModel()

@Binding var showDeviceAuthorizationView: Bool

init(showDeviceAuthorizationView: Binding<Bool>) {
self._showDeviceAuthorizationView = showDeviceAuthorizationView
}

var body: some View {
VStack(spacing: 16.0) {
switch viewModel.state {
case .initial:
Text("daf.label.loading")
case .deviceCodeAvailable(let deviceCode):
LabelItem("daf.label.usercode", value: deviceCode.userCode)
LabelItem("daf.label.verificationuri", value: deviceCode.verificationUri)
LabelItem("daf.label.verficationuricomplete", value: deviceCode.verificationUriComplete ?? "")

if let verificationUriCompleteImage = viewModel.verificationUriCompleteImage {
Image(uiImage: verificationUriCompleteImage)
.resizable()
.frame(width: 200, height: 200)
}
Button("daf.label.cancel") {
viewModel.cancelSignIn()
}
case .deviceCodeExpired:
Text("daf.label.expired")
Button("daf.label.dismiss") {
showDeviceAuthorizationView = false
}
case .error(let error):
switch error {
case OpenPassError.authorizationCancelled:
Text("daf.label.user_cancelled")
default:
Text("Error: \(error.localizedDescription)")
}
Button("daf.label.dismiss") {
showDeviceAuthorizationView = false
}
case .complete:
Text("daf.label.complete")
Button("daf.label.dismiss") {
showDeviceAuthorizationView = false
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal, 16.0)
.onAppear(perform: {
viewModel.startSignInFlow()
})
}
}

private struct LabelItem: View {
var label: LocalizedStringKey
var value: String

init(_ label: LocalizedStringKey, value: String) {
self.label = label
self.value = value
}

var body: some View {
Text(label)
.fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .leading)
Text(value)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
#endif
120 changes: 120 additions & 0 deletions Development/OpenPassDevelopmentApp/DeviceAuthorizationViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//
// DeviceAuthorizationViewModel.swift
//
// MIT License
//
// Copyright (c) 2024 The Trade Desk (https://www.thetradedesk.com/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//

#if os(tvOS)

import CoreImage.CIFilterBuiltins
import Foundation
import OpenPass
import SwiftUI

extension DeviceAuthorizationViewModel {

/// A interface defining the flow of state communicated by the `DeviceAuthorizationFlow`
public enum State: Sendable {

/// The client has been initialized but a `DeviceCode` has not been requested or received
case initial

/// A `DeviceCode` is loaded
case deviceCodeAvailable(DeviceCode)

/// The flow is complete and the associated `OpenPassManager` has obtained the set of `OpenPassTokens`.
case complete(OpenPassTokens)

/// The previous `DeviceCode` has expired and the flow should be restarted.
case deviceCodeExpired

/// An error has occurred.
case error(Error)
}
}

@MainActor
final class DeviceAuthorizationViewModel: ObservableObject {

/// A QR Code representation of `deviceCode.verificationUriComplete`, if available
@Published private(set) var verificationUriCompleteImage: UIImage?

@Published private(set) var state: DeviceAuthorizationViewModel.State = .initial {
didSet {
if case .deviceCodeAvailable(let deviceCode) = state {
verificationUriCompleteImage = deviceCode.verificationUriComplete.flatMap(UIImage.qrCode(url: ))
} else {
verificationUriCompleteImage = nil
}
}
}

private var signInTask: Task<Void, Never>?

public func startSignInFlow() {
cancelSignIn()
state = .initial

signInTask = Task {
let flow = OpenPassManager.shared.deviceAuthorizationFlow

do {
// Request a Device Code
let deviceCode = try await flow.fetchDeviceCode()
state = .deviceCodeAvailable(deviceCode)

// Poll for authorization
let tokens = try await flow.fetchAccessToken(deviceCode: deviceCode)
state = .complete(tokens)
} catch OpenPassError.tokenExpired {
state = .deviceCodeExpired
} catch {
state = .error(error)
}
}
}

public func cancelSignIn() {
signInTask?.cancel()
signInTask = nil
}
}

extension UIImage {
static func qrCode(url: String) -> UIImage? {
guard let data = url.data(using: String.Encoding.ascii) else {
return nil
}
let filter = CIFilter.qrCodeGenerator()
filter.message = data

let transform = CGAffineTransform(scaleX: 10, y: 10)
guard let output = filter.outputImage?.transformed(by: transform) else {
return nil
}
// The filter's outputImage isn't suitable for rendering as-is. Convert to PNG and back.
return UIImage(ciImage: output).pngData()
.flatMap(UIImage.init(data: ))
}
}
#endif
13 changes: 13 additions & 0 deletions Development/OpenPassDevelopmentApp/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,22 @@
"common.nil" = "nil";
"common.openpasssdk" = "OpenPass SDK";

"daf.label.loading" = "Loading Device Code";
"daf.label.expired" = "Device Code expired";
"daf.label.complete" = "Success";
"daf.label.cancel" = "Cancel";
"daf.label.dismiss" = "Back to Tokens";
"daf.label.user_cancelled" = "User cancelled";
"daf.label.error" = "Error: %@";

"daf.label.usercode" = "User Code";
"daf.label.verificationuri" = "Verification URI";
"daf.label.verficationuricomplete" = "Verification URI Complete";

"root.button.signout" = "Sign Out";
"root.button.signin" = "Sign In";
"root.button.refresh" = "Refresh";
"root.button.daf" = "Sign In (Device Auth)";

"root.title.openpassTokens" = "OpenPass Tokens";

Expand Down

This file was deleted.

Loading
Loading