Skip to content

Commit

Permalink
oauth + login
Browse files Browse the repository at this point in the history
  • Loading branch information
Aayush Pokharel authored and Aayush Pokharel committed Dec 23, 2023
1 parent 13fe6d8 commit ece417d
Show file tree
Hide file tree
Showing 18 changed files with 340 additions and 166 deletions.
20 changes: 8 additions & 12 deletions NativeTwitch.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
457128462B2AAFC700838150 /* AuthModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457128452B2AAFC700838150 /* AuthModel.swift */; };
457128482B2AB2EA00838150 /* OauthValidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457128472B2AB2EA00838150 /* OauthValidate.swift */; };
45A401722B2E12F900CFA6CC /* BadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A401712B2E12F900CFA6CC /* BadgeView.swift */; };
45AE499D2B3193DB00B6CBEF /* TwitchDeviceAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE499C2B3193DB00B6CBEF /* TwitchDeviceAuth.swift */; };
45B914182B2D17D500B8D3D1 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B914172B2D17D500B8D3D1 /* LoginView.swift */; };
45B9141B2B2D1BFF00B8D3D1 /* LongButtonModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B9141A2B2D1BFF00B8D3D1 /* LongButtonModifier.swift */; };
45B9141D2B2D1D0500B8D3D1 /* CleanTextFieldStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B9141C2B2D1D0400B8D3D1 /* CleanTextFieldStyle.swift */; };
Expand All @@ -37,7 +38,6 @@
45B9143A2B2D3C7600B8D3D1 /* AnimatedGradientFill.metal in Sources */ = {isa = PBXBuildFile; fileRef = 45B914362B2D3C5500B8D3D1 /* AnimatedGradientFill.metal */; };
45B9143B2B2D3C7600B8D3D1 /* Sinebow.metal in Sources */ = {isa = PBXBuildFile; fileRef = 45B914352B2D3C5500B8D3D1 /* Sinebow.metal */; };
45B9143C2B2D3C7900B8D3D1 /* LightGrid.metal in Sources */ = {isa = PBXBuildFile; fileRef = 45B914372B2D3C5500B8D3D1 /* LightGrid.metal */; };
45D1F1E62B2FBC0C0061D2C5 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D1F1E52B2FBC0C0061D2C5 /* SettingsView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -62,6 +62,8 @@
457128452B2AAFC700838150 /* AuthModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthModel.swift; sourceTree = "<group>"; };
457128472B2AB2EA00838150 /* OauthValidate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OauthValidate.swift; sourceTree = "<group>"; };
45A401712B2E12F900CFA6CC /* BadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeView.swift; sourceTree = "<group>"; };
45AE499B2B317FE600B6CBEF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
45AE499C2B3193DB00B6CBEF /* TwitchDeviceAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitchDeviceAuth.swift; sourceTree = "<group>"; };
45B914172B2D17D500B8D3D1 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
45B9141A2B2D1BFF00B8D3D1 /* LongButtonModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongButtonModifier.swift; sourceTree = "<group>"; };
45B9141C2B2D1D0400B8D3D1 /* CleanTextFieldStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanTextFieldStyle.swift; sourceTree = "<group>"; };
Expand All @@ -73,7 +75,6 @@
45B914362B2D3C5500B8D3D1 /* AnimatedGradientFill.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = AnimatedGradientFill.metal; sourceTree = "<group>"; };
45B914372B2D3C5500B8D3D1 /* LightGrid.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = LightGrid.metal; sourceTree = "<group>"; };
45B914382B2D3C5500B8D3D1 /* GenerativePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerativePreview.swift; sourceTree = "<group>"; };
45D1F1E52B2FBC0C0061D2C5 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -106,6 +107,7 @@
450871C42B291C4B00F938A2 /* NativeTwitch */ = {
isa = PBXGroup;
children = (
45AE499B2B317FE600B6CBEF /* Info.plist */,
450871C52B291C4B00F938A2 /* NativeTwitchApp.swift */,
450871C72B291C4B00F938A2 /* ContentView.swift */,
450871D52B291C5500F938A2 /* Views */,
Expand Down Expand Up @@ -147,7 +149,6 @@
450871D52B291C5500F938A2 /* Views */ = {
isa = PBXGroup;
children = (
45D1F1E42B2FBBF30061D2C5 /* SettingsView */,
45B914232B2D27AC00B8D3D1 /* Subviews */,
45B914262B2D29CF00B8D3D1 /* StreamsView.swift */,
45B914282B2D308500B8D3D1 /* SingleStreamRow.swift */,
Expand All @@ -172,6 +173,7 @@
isa = PBXGroup;
children = (
457128432B2AAE9A00838150 /* TwitchVM.swift */,
45AE499C2B3193DB00B6CBEF /* TwitchDeviceAuth.swift */,
);
path = ViewModels;
sourceTree = "<group>";
Expand Down Expand Up @@ -214,14 +216,6 @@
path = Shaders;
sourceTree = "<group>";
};
45D1F1E42B2FBBF30061D2C5 /* SettingsView */ = {
isa = PBXGroup;
children = (
45D1F1E52B2FBC0C0061D2C5 /* SettingsView.swift */,
);
path = SettingsView;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -314,7 +308,6 @@
457128482B2AB2EA00838150 /* OauthValidate.swift in Sources */,
4571283C2B2AA90700838150 /* Streams.swift in Sources */,
45B914392B2D3C6B00B8D3D1 /* GenerativePreview.swift in Sources */,
45D1F1E62B2FBC0C0061D2C5 /* SettingsView.swift in Sources */,
45B9142B2B2D36B800B8D3D1 /* LinearGradient+Extensions.swift in Sources */,
450871DD2B291C8100F938A2 /* View+Extension.swift in Sources */,
45B914182B2D17D500B8D3D1 /* LoginView.swift in Sources */,
Expand All @@ -325,6 +318,7 @@
45B9142D2B2D375200B8D3D1 /* TransitionModifier.swift in Sources */,
450871DE2B291C8100F938A2 /* Double+Extension.swift in Sources */,
450871C62B291C4B00F938A2 /* NativeTwitchApp.swift in Sources */,
45AE499D2B3193DB00B6CBEF /* TwitchDeviceAuth.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -463,6 +457,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = NativeTwitch/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NativeTwitch;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
Expand Down Expand Up @@ -492,6 +487,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = NativeTwitch/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NativeTwitch;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "transparentIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions NativeTwitch/Assets.xcassets/twitchLogo.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion NativeTwitch/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ struct ContentView: View {
.task {
await twitchVM.fetchFollowedStreams()
}

.onKeyboardShortcut(key: "r", modifiers: .command) {
Task {
await twitchVM.fetchFollowedStreams()
Expand Down
2 changes: 1 addition & 1 deletion NativeTwitch/Helpers/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.prohibited)
// NSApp.setActivationPolicy(.prohibited)
}
}
19 changes: 19 additions & 0 deletions NativeTwitch/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>nativetwitch</string>
<key>CFBundleURLSchemes</key>
<array>
<string>nativetwitch</string>
</array>
</dict>
</array>
</dict>
</plist>
5 changes: 4 additions & 1 deletion NativeTwitch/Models/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

enum Constants {
static let donateLink = "https://www.buymeacoffee.com/swiftdev".toURL()!
static let baseAPI = "https://api.twitch.tv/helix"
static let followedAPI = "\(baseAPI)/streams/followed"
static let streamerInfoURL = "\(baseAPI)/users".toURL()!
Expand All @@ -17,5 +18,7 @@ enum Constants {
return "\(followedAPI)?user_id=\(userID)".toURL()
}

static let tokenGeneratorURL = "https://twitchtokengenerator.com/quick/NIaMdzGYBR".toURL()!
// Oauth flow
static let clientID = "gp762nuuoqcoxypju8c569th9wz7q5"
static let scopes = ["user:read:follows", "user:read:email", "user:edit:follows"].joined(separator: "+")
}
18 changes: 11 additions & 7 deletions NativeTwitch/NativeTwitchApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,23 @@ struct NativeTwitchApp: App {
.commands {
CommandGroup(replacing: CommandGroupPlacement.appInfo) {
Button("About NativeTwitch") { appDelegate.showAboutPanel() }
.keyboardShortcut(KeyEquivalent("i"), modifiers: .command)
}
if twitchVM.loggedIn {
CommandGroup(after: .appInfo) {
CommandGroup(replacing: .systemServices) {
Button("Hide Application, Maintain Menu Bar") {
twitchVM.showOnlyMenu.toggle()
NSApp.setActivationPolicy(.prohibited)
}
.keyboardShortcut(KeyEquivalent("q"), modifiers: .option)
}
CommandGroup(replacing: .appVisibility) {
if twitchVM.loggedIn {
Button("Log Out") {
twitchVM.logout()
}
.keyboardShortcut(KeyEquivalent("q"), modifiers: .shift)
}
}
}

Settings {
SettingsView()
.frame(width: 480, height: 320)
}
}
}
58 changes: 58 additions & 0 deletions NativeTwitch/ViewModels/TwitchDeviceAuth.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// TwitchDeviceAuth.swift
// NativeTwitch
//
// Created by Aayush Pokharel on 2023-12-19.
//

import Foundation
import os

class TwitchDeviceAuth {
let logger = Logger(category: "🔑")

let clientID: String = Constants.clientID
let scope: String = Constants.scopes

func startDeviceAuthorization() async throws -> (deviceCode: String, userCode: String, verificationUri: String) {
logger.log("Starting Device Authorization")
let url = URL(string: "https://id.twitch.tv/oauth2/device")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let bodyParameters = "client_id=\(clientID)&scopes=\(scope)"
request.httpBody = bodyParameters.data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
print("Start Device Authorization \(json)")
guard let deviceCode = json["device_code"] as? String,
let userCode = json["user_code"] as? String,
let verificationUri = json["verification_uri"] as? String
else {
throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response data"])
}

return (deviceCode, userCode, verificationUri)
}

func pollForToken(deviceCode: String) async throws -> String {
logger.log("Polling for Token")
let url = URL(string: "https://id.twitch.tv/oauth2/token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let bodyParameters = "client_id=\(clientID)&device_code=\(deviceCode)&grant_type=urn:ietf:params:oauth:grant-type:device_code"
request.httpBody = bodyParameters.data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
print("pollForToken \(json)")
if let accessToken = json["access_token"] as? String {
// Store the access token securely
return accessToken
} else {
throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Authorization pending or other error"])
}
}
}
65 changes: 56 additions & 9 deletions NativeTwitch/ViewModels/TwitchVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,22 @@ import SwiftUI
@Observable
class TwitchVM {
private let logger: Logger = .init(category: "TwitchVM")
var twitchAuth: TwitchDeviceAuth = .init()
var deviceCodeInfo: (userCode: String, verificationUri: String)?

var loggedIn: Bool = true
var streams: [StreamModel] = []

// Login
var attempts = 0
let maxAttempts = 25

// UI
var showOnlyMenu = false

init() {
print("CREATED TWITCH VM")
loggedIn = (KeychainSwift.getUserID() != nil)
}

var loading = false {
Expand All @@ -33,23 +44,59 @@ class TwitchVM {
}
}

func startDeviceAuthorization() async {
do {
attempts = 1
let (deviceCode, userCode, verificationUri) = try await twitchAuth.startDeviceAuthorization()
deviceCodeInfo = (userCode, verificationUri)
await pollForToken(deviceCode: deviceCode)
} catch {
logger.error("Device Authorization Error: \(error.localizedDescription)")
}
}

private func pollForToken(deviceCode: String) async {
let pollingIntervalNanoseconds: UInt64 = 5_000_000_000 // 5 seconds

while attempts < maxAttempts {
do {
let accessToken = try await twitchAuth.pollForToken(deviceCode: deviceCode)
let authModel = AuthModel(Constants.clientID, accessToken)
let loginSuccess = KeychainSwift.login(authModel)
loggedIn = loginSuccess
if loginSuccess {
await fetchFollowedStreams()
return
}
} catch {
logger.error("Error Polling for Token: \(error.localizedDescription)")
}
attempts += 1
try? await Task.sleep(nanoseconds: pollingIntervalNanoseconds)
}

logger.error("Max polling attempts reached or device code expired")
}

@MainActor
func login(with auth: AuthModel) {
logger.log("Logging in with \(auth.accessToken) & \(auth.clientID)")
loggedIn = KeychainSwift.login(auth)
Task {
await fetchFollowedStreams()
func login() async {
guard let auth = KeychainSwift.getAuth() else {
loggedIn = false
return
}

logger.log("Logging in with \(auth.accessToken) & \(auth.clientID)")
loggedIn = true
await fetchFollowedStreams()
}

@MainActor
func logout() {
loggedIn = !KeychainSwift.logout()
Task {
streams = []
}
streams = []
deviceCodeInfo = nil
}

@MainActor
func fetchFollowedStreams() async {
loading = true
Expand Down
2 changes: 1 addition & 1 deletion NativeTwitch/ViewModifiers/LongButtonModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct LongButtonModifier: ViewModifier {
.padding(8)
.foregroundStyle(foreground)
.background(background)
.clipShape(.rect(cornerRadius: 8))
.clipShape(.rect(cornerRadius: radius))
.fontWeight(.semibold)
}
}
Expand Down
Loading

0 comments on commit ece417d

Please sign in to comment.