Skip to content

Commit 17b65be

Browse files
authored
Merge pull request #16 from daneden/feat-oauth2
Add OAuth 2.0 user authentication
2 parents a3dc5b1 + da4c0b2 commit 17b65be

File tree

10 files changed

+309
-82
lines changed

10 files changed

+309
-82
lines changed

Demo App/Twift_SwiftUI/Tweets/PostTweet.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ struct PostTweet: View {
2727

2828
AsyncButton {
2929
do {
30-
let media = MutableMedia(mediaIds: [mediaKey])
30+
let media = mediaKey.isEmpty ? nil : MutableMedia(mediaIds: [mediaKey])
3131
let tweet = MutableTweet(text: text, media: media)
3232

3333
let response = try await twitterClient.postTweet(tweet)
3434

3535
tweetId = response.data.id
36+
37+
text = ""
38+
mediaKey = ""
3639
} catch {
3740
print(error)
3841
}

Demo App/Twift_SwiftUI/Tweets/UserLikes.swift

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Twift
1010

1111
struct UserLikes: View {
1212
@EnvironmentObject var twitterClient: Twift
13-
@State var tweets: [Tweet]?
13+
@State var tweets: [Tweet] = []
1414
@State var errors: [TwitterAPIError] = []
1515
@State var meta: Meta?
1616
@State var includes: Tweet.Includes?
@@ -24,27 +24,7 @@ struct UserLikes: View {
2424
.keyboardType(.numberPad)
2525

2626
AsyncButton(action: {
27-
do {
28-
let result = try await twitterClient.getLikedTweets(
29-
for: userId,
30-
fields: Set(Tweet.publicFields),
31-
expansions: [.authorId(userFields: [\.profileImageUrl])]
32-
)
33-
34-
withAnimation {
35-
tweets = result.data
36-
includes = result.includes
37-
errors = result.errors ?? []
38-
}
39-
} catch {
40-
if let error = error as? TwitterAPIError {
41-
withAnimation { errors = [error] }
42-
} else if let error = (error as? TwitterAPIManyErrors)?.errors {
43-
withAnimation { errors = error }
44-
} else {
45-
print(error.localizedDescription)
46-
}
47-
}
27+
await getPage()
4828
}) {
4929
Text("Get user likes")
5030
}
@@ -59,13 +39,14 @@ struct UserLikes: View {
5939
}.navigationTitle("Get User Likes")
6040
}
6141

62-
func getPage(_ token: String?) async {
42+
func getPage(_ token: String? = nil) async {
6343
do {
6444
let result = try await twitterClient.getLikedTweets(
6545
for: userId,
6646
fields: Set(Tweet.publicFields),
6747
expansions: [.authorId(userFields: [\.profileImageUrl])],
68-
paginationToken: token
48+
paginationToken: token,
49+
maxResults: 100
6950
)
7051

7152
withAnimation {
@@ -78,7 +59,7 @@ struct UserLikes: View {
7859
if let error = error as? TwitterAPIError {
7960
withAnimation { errors = [error] }
8061
} else {
81-
print(error.localizedDescription)
62+
print(error)
8263
}
8364
}
8465
}

Demo App/Twift_SwiftUI/Twift_SwiftUIApp.swift

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ extension Twift {
1313
switch authenticationType {
1414
case .appOnly(_): return false
1515
case .userAccessTokens(_, _): return true
16+
case .oauth2UserAuth(_): return true
1617
}
1718
}
1819
}
@@ -40,20 +41,18 @@ struct Twift_SwiftUIApp: App {
4041
NavigationView {
4142
Form {
4243
Section(
43-
header: Text("User Access Tokens"),
44-
footer: Text("Use this authentication method for most cases.")
44+
header: Text("OAuth 2.0 User Authentication"),
45+
footer: Text("Use this authentication method for most cases. This test app enables all user scopes by default.")
4546
) {
46-
Button {
47-
Twift.Authentication().requestUserCredentials(clientCredentials: clientCredentials, callbackURL: URL(string: TWITTER_CALLBACK_URL)!) { (userCredentials, error) in
48-
if let error = error {
49-
print(error.localizedDescription)
50-
}
47+
AsyncButton {
48+
let (user, _) = await Twift.Authentication().authenticateUser(clientId: "Sm5PSUhRNW9EZ3NXb0tJQkI5WU06MTpjaQ",
49+
redirectUri: URL(string: TWITTER_CALLBACK_URL)!,
50+
scope: Set(OAuth2Scope.allCases))
51+
52+
if let user = user {
53+
container.client = Twift(.oauth2UserAuth(user))
5154

52-
if let creds = userCredentials {
53-
DispatchQueue.main.async {
54-
container.client = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: creds))
55-
}
56-
}
55+
try? await container.client?.refreshOAuth2AccessToken()
5756
}
5857
} label: {
5958
Text("Sign In With Twitter")
@@ -62,7 +61,7 @@ struct Twift_SwiftUIApp: App {
6261

6362
Section(
6463
header: Text("App-Only Bearer Token"),
65-
footer: Text("Use this authentication method for app-only methods such as filtered streams")
64+
footer: Text("Use this authentication method for app-only methods such as filtered streams.")
6665
) {
6766
TextField("Enter Bearer Token", text: $bearerToken)
6867
Button {

README.md

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,35 @@
66
Twift is an asynchronous Swift library for the Twitter v2 API.
77

88
- [x] No external dependencies
9-
- [x] Only one callback-based method ([`Authentication.requestUserCredentials`](https://github.com/daneden/Twift/wiki/Twift_Authentication#requestusercredentialsclientcredentialscallbackurlpresentationcontextproviderwith))
9+
- [x] Fully async
1010
- [x] Full Swift type definitions/wrappers around Twitter's API objects
1111

1212
## Quick Start Guide
1313

14-
New `Twift` instances must be initiated with either User Access Tokens or an App-Only Bearer Token:
14+
New `Twift` instances must be initiated with either OAuth 2.0 user authentication or an App-Only Bearer Token:
1515

1616
```swift
1717
// User access tokens
18-
let clientCredentials = OAuthCredential(key: CONSUMER_KEY, secret: CONSUMER_SECRET)
19-
let userCredentials = OAuthCredential(key: ACCESS_KEY, secret: ACCESS_SECRET)
20-
let userAuthenticatedClient = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: userCredentials)
18+
let oauthUser: OAuth2User = OAUTH2_USER
19+
let userAuthenticatedClient = Twift(.oauth2UserAuth(oauthUser: oauthUser)
2120

2221
// Bearer token
2322
let appOnlyClient = Twift(.appOnly(bearerToken: BEARER_TOKEN)
2423
```
2524

26-
You can acquire user access tokens by authenticating the user with `Twift.Authentication().requestUserCredentials()`:
25+
You can authenticating users with `Twift.Authentication().authenticateUser()`:
2726

2827
```swift
2928
var client: Twift?
3029

31-
Twift.Authentication().requestUserCredentials(
32-
clientCredentials: clientCredentials,
33-
callbackURL: URL(string: "twift-test://")!
34-
) { (userCredentials, error) in
35-
if let creds = userCredentials {
36-
client = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: creds))
37-
}
30+
let (oauthUser, error) = await Twift.Authentication().authenticateUser(
31+
clientId: TWITTER_CLIENT_ID,
32+
redirectUri: URL(string: TWITTER_CALLBACK_URL)!,
33+
scope: Set(OAuth2Scope.allCases)
34+
)
35+
36+
if let oauthUser = oauthUser {
37+
client = Twift(.oauth2UserAuth(oauthUser))
3838
}
3939
```
4040

@@ -125,29 +125,3 @@ let me = response?.data
125125
// The user's pinned Tweet
126126
let tweet = response?.includes?.tweets.first
127127
```
128-
129-
### Optional Actor IDs
130-
131-
Many of Twift's methods require a `User.ID` in order to make requests on behalf of that user. For convenience, this parameter is often marked as optional, since the currently-authenticated `User.ID` may be found on the instance's authentication type:
132-
133-
```swift
134-
var client: Twift?
135-
var credentials: OAuthCredential?
136-
137-
Twift.Authentication().requestUserCredentials(
138-
clientCredentials: clientCredentials,
139-
callbackURL: URL(string: "twift-test://")!
140-
) { (userCredentials, error) in
141-
if let userCredentials = userCredentials {
142-
client = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: userCredentials))
143-
credentials = userCredentials
144-
}
145-
}
146-
147-
// Elsewhere in the app...
148-
149-
// These two calls are equivalent since the client was initialized with an OAuthCredential containing the authenticated user's ID
150-
let result1 = try? await client?.followUser(sourceUserId: credentials.userId!, targetUserId: "12")
151-
let result2 = try? await client?.followUser(targetUserId: "12")
152-
153-
```

Sources/Twift+API.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ extension Twift {
88
body: Data? = nil,
99
expectedReturnType: T.Type
1010
) async throws -> T {
11+
if case AuthenticationType.oauth2UserAuth(_) = self.authenticationType {
12+
try await self.refreshOAuth2AccessToken()
13+
}
14+
1115
let url = getURL(for: route, queryItems: queryItems)
1216
var request = URLRequest(url: url)
1317

@@ -17,7 +21,7 @@ extension Twift {
1721
}
1822

1923
signURLRequest(method: method, body: body, request: &request)
20-
24+
2125
let (data, _) = try await URLSession.shared.data(for: request)
2226

2327
return try decodeOrThrow(decodingType: T.self, data: data)
@@ -70,7 +74,11 @@ extension Twift {
7074
consumerCredentials: clientCredentials,
7175
userCredentials: userCredentials
7276
)
77+
case .oauth2UserAuth(let oauthUser):
78+
request.addValue("Bearer \(oauthUser.accessToken)", forHTTPHeaderField: "Authorization")
7379
}
80+
81+
request.httpMethod = method.rawValue
7482
}
7583
}
7684

0 commit comments

Comments
 (0)