Skip to content

Commit b50ab83

Browse files
authored
feat: add access token methods (#144)
* feat: add access token methods * ci: update destination * refactor: apply code review suggestions * refactor: fix test cases
1 parent dba981d commit b50ab83

File tree

14 files changed

+562
-153
lines changed

14 files changed

+562
-153
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
set -o pipefail && \
3939
xcodebuild \
4040
-scheme "LogtoSDK-Package" \
41-
-destination "platform=iOS Simulator,OS=26.1,name=iPhone 17 Pro" \
41+
-destination "platform=iOS Simulator,OS=26.2,name=iPhone 17 Pro" \
4242
-enableCodeCoverage=YES \
4343
-resultBundlePath Logto.xcresult \
4444
test | \

Demos/SwiftUI Demo/SwiftUI Demo.xcodeproj/project.pbxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@
434434
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
435435
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
436436
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
437+
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
437438
LD_RUNPATH_SEARCH_PATHS = (
438439
"$(inherited)",
439440
"@executable_path/Frameworks",
@@ -466,6 +467,7 @@
466467
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
467468
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
468469
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
470+
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
469471
LD_RUNPATH_SEARCH_PATHS = (
470472
"$(inherited)",
471473
"@executable_path/Frameworks",

Demos/SwiftUI Demo/SwiftUI Demo/ContentView.swift

Lines changed: 216 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -9,144 +9,247 @@ import Logto
99
import LogtoClient
1010
import SwiftUI
1111

12-
struct ContentView: View {
13-
@State var isAuthenticated: Bool
14-
@State var authError: Error?
12+
// MARK: - 1) Edit these values
13+
14+
enum DemoAuthConfig {
15+
static let endpoint = "<YOUR_LOGTO_ENDPOINT>"
16+
static let appId = "<YOUR_APP_ID>"
17+
static let redirectUri = "<YOUR_REDIRECT_URI>" // e.g. "io.logto://callback"
18+
19+
// MARK: Optional config items
20+
21+
static let resources: [String] = [
22+
"<YOUR_API_RESOURCE>", // e.g. "https://api.example.com"
23+
]
24+
25+
static let resourceToRequestTokenFor = "<YOUR_API_RESOURCE>"
26+
static let organizationId = "<YOUR_ORGANIZATION_ID>"
27+
28+
static let scopes: [String] = [
29+
UserScope.email.rawValue,
30+
UserScope.roles.rawValue,
31+
UserScope.organizations.rawValue,
32+
UserScope.organizationRoles.rawValue,
33+
]
34+
}
35+
36+
// MARK: - 2) ViewModel
37+
38+
@MainActor
39+
final class DemoAuthViewModel: ObservableObject {
40+
@Published var isAuthenticated = false
41+
@Published var lastError: String?
42+
@Published var output: String = ""
1543

16-
let resource = "https://api.logto.io"
17-
let client: LogtoClient?
44+
private let client: LogtoClient?
45+
var isConfigured: Bool { client != nil }
1846

1947
init() {
48+
let c = Self.makeClient()
49+
client = c
50+
isAuthenticated = c?.isAuthenticated ?? false
51+
52+
if c == nil {
53+
log("config invalid: please update DemoAuthConfig placeholders")
54+
} else if c?.isAuthenticated == true {
55+
log("already authenticated")
56+
}
57+
}
58+
59+
private static func makeClient() -> LogtoClient? {
2060
guard let config = try? LogtoConfig(
21-
endpoint: "<your-logto-endpoint>",
22-
appId: "<your-application-id>",
23-
// Update per your needs
24-
scopes: [
25-
UserScope.email.rawValue,
26-
UserScope.roles.rawValue,
27-
UserScope.organizations.rawValue,
28-
UserScope.organizationRoles.rawValue,
29-
],
30-
// Update per your needs
31-
resources: []
61+
endpoint: DemoAuthConfig.endpoint,
62+
appId: DemoAuthConfig.appId,
63+
scopes: DemoAuthConfig.scopes,
64+
resources: DemoAuthConfig.resources
3265
) else {
33-
client = nil
66+
return nil
67+
}
68+
return LogtoClient(useConfig: config)
69+
}
70+
71+
// MARK: Actions
72+
73+
func signIn() async {
74+
guard let client else { logNotConfigured(); return }
75+
clearError()
76+
do {
77+
try await client.signInWithBrowser(redirectUri: DemoAuthConfig.redirectUri)
78+
isAuthenticated = true
79+
log("sign-in success")
80+
} catch {
3481
isAuthenticated = false
35-
return
82+
handle(error, context: "sign-in")
3683
}
37-
let logtoClient = LogtoClient(useConfig: config)
38-
client = logtoClient
39-
isAuthenticated = logtoClient.isAuthenticated
84+
}
4085

41-
if logtoClient.isAuthenticated {
42-
print("authed", logtoClient.refreshToken ?? "N/A")
86+
func signOut() async {
87+
guard let client else { logNotConfigured(); return }
88+
clearError()
89+
await client.signOut()
90+
isAuthenticated = false
91+
log("signed out")
92+
}
93+
94+
func printIdTokenClaims() {
95+
guard let client else { logNotConfigured(); return }
96+
clearError()
97+
do {
98+
let claims = try client.getIdTokenClaims()
99+
log("id token claims:\n\(claims)")
100+
} catch {
101+
handle(error, context: "get id token claims")
43102
}
44103
}
45104

46-
var body: some View {
47-
Text("Hello, world!")
48-
.padding()
49-
if isAuthenticated {
50-
Text("Signed In")
51-
.padding()
105+
func fetchUserInfo() async {
106+
guard let client else { logNotConfigured(); return }
107+
clearError()
108+
do {
109+
let userInfo = try await client.fetchUserInfo()
110+
log("userinfo:\n\(userInfo)")
111+
} catch {
112+
handle(error, context: "fetch userinfo")
52113
}
53-
if let authError = authError {
54-
Text(authError.localizedDescription)
55-
.foregroundColor(.red)
56-
.padding()
114+
}
115+
116+
func fetchAccessTokenClaims(for resource: String) async {
117+
guard let client else { logNotConfigured(); return }
118+
clearError()
119+
do {
120+
let claims = try await client.getAccessTokenClaims(for: resource)
121+
log("access token claims for \(resource):\n\(claims)")
122+
} catch {
123+
handle(error, context: "get access token claims")
57124
}
125+
}
58126

59-
if let client = client {
60-
Button("Print ID Token Claims") {
61-
print(try! client.getIdTokenClaims())
62-
}
63-
Button("Sign In") {
64-
Task { [self] in
65-
do {
66-
try await client.signInWithBrowser(redirectUri: "io.logto://callback")
67-
68-
isAuthenticated = true
69-
authError = nil
70-
} catch let error as LogtoClientErrors.SignIn {
71-
isAuthenticated = false
72-
authError = error
73-
74-
print("failure", error)
75-
76-
if let error = error.innerError as? LogtoErrors.Response,
77-
case let LogtoErrors.Response.withCode(
78-
_,
79-
_,
80-
data
81-
) = error, let data = data
82-
{
83-
print(String(decoding: data, as: UTF8.self))
84-
}
85-
} catch {
86-
print(error)
127+
func fetchOrganizationTokenClaims(for organizationId: String) async {
128+
guard let client else { logNotConfigured(); return }
129+
clearError()
130+
do {
131+
let claims = try await client.getOrganizationTokenClaims(forId: organizationId)
132+
log("organization token claims for \(organizationId):\n\(claims)")
133+
} catch {
134+
handle(error, context: "get organization token claims")
135+
}
136+
}
137+
138+
func fetchAccessTokenClaimsInOrg(resource: String, organizationId: String) async {
139+
guard let client else { logNotConfigured(); return }
140+
clearError()
141+
do {
142+
let claims = try await client.getAccessTokenClaims(for: resource, organizationId: organizationId)
143+
log("access token claims for \(resource) in org \(organizationId):\n\(claims)")
144+
} catch {
145+
handle(error, context: "get access token claims in org")
146+
}
147+
}
148+
149+
// MARK: Helpers
150+
151+
private func clearError() {
152+
lastError = nil
153+
}
154+
155+
private func log(_ message: String) {
156+
output = message
157+
print(message)
158+
}
159+
160+
private func logNotConfigured() {
161+
lastError = "Logto is not configured. Please update DemoAuthConfig."
162+
log(lastError!)
163+
}
164+
165+
private func handle(_ error: Error, context: String) {
166+
var msg = "[\(context)] \(error.localizedDescription)"
167+
168+
if let signInErr = error as? LogtoClientErrors.SignIn,
169+
let resp = signInErr.innerError as? LogtoErrors.Response,
170+
case let LogtoErrors.Response.withCode(_, _, data) = resp,
171+
let data = data
172+
{
173+
msg += "\n\nresponse:\n" + String(decoding: data, as: UTF8.self)
174+
}
175+
176+
lastError = msg
177+
log(msg)
178+
}
179+
}
180+
181+
// MARK: - 3) UI
182+
183+
struct ContentView: View {
184+
@StateObject private var vm = DemoAuthViewModel()
185+
186+
var body: some View {
187+
NavigationView {
188+
Form {
189+
Section("Status") {
190+
HStack {
191+
Text("Configured")
192+
Spacer()
193+
Text(vm.isConfigured ? "Yes" : "No")
194+
.foregroundColor(vm.isConfigured ? .green : .secondary)
87195
}
88-
}
89-
}
90196

91-
Button("Sign Out") {
92-
Task {
93-
await client.signOut()
94-
self.isAuthenticated = false
95-
}
96-
}
197+
HStack {
198+
Text("Authenticated")
199+
Spacer()
200+
Text(vm.isAuthenticated ? "Yes" : "No")
201+
.foregroundColor(vm.isAuthenticated ? .green : .secondary)
202+
}
97203

98-
Button("Fetch Userinfo") {
99-
Task {
100-
do {
101-
let userInfo = try await client.fetchUserInfo()
102-
print(userInfo)
103-
} catch let error as LogtoClientErrors.UserInfo {
104-
if let error = error.innerError as? LogtoClientErrors.AccessToken,
105-
let error = error.innerError as? LogtoErrors.Response,
106-
case let LogtoErrors.Response.withCode(
107-
_,
108-
_,
109-
data
110-
) = error, let data = data
111-
{
112-
print(String(decoding: data, as: UTF8.self))
113-
} else {
114-
print(error)
115-
}
116-
} catch {
117-
print(error)
204+
if let err = vm.lastError {
205+
Text(err)
206+
.foregroundColor(.red)
207+
.font(.footnote)
208+
.textSelection(.enabled)
118209
}
119210
}
120-
}
121211

122-
Button("Fetch access token for \(resource)") {
123-
Task {
124-
do {
125-
let token = try await client.getAccessToken(for: resource)
126-
print(token)
127-
} catch {
128-
print(error)
212+
Section("Actions") {
213+
Button("Sign In") { Task { await vm.signIn() } }
214+
.disabled(!vm.isConfigured || vm.isAuthenticated)
215+
216+
Button("Sign Out") { Task { await vm.signOut() } }
217+
.disabled(!vm.isConfigured || !vm.isAuthenticated)
218+
219+
Button("Print ID Token Claims") { vm.printIdTokenClaims() }
220+
.disabled(!vm.isConfigured || !vm.isAuthenticated)
221+
222+
Button("Fetch Userinfo") { Task { await vm.fetchUserInfo() } }
223+
.disabled(!vm.isConfigured || !vm.isAuthenticated)
224+
225+
Button("Fetch access token claims") {
226+
Task { await vm.fetchAccessTokenClaims(for: DemoAuthConfig.resourceToRequestTokenFor) }
129227
}
130-
}
131-
}
228+
.disabled(!vm.isConfigured || !vm.isAuthenticated)
132229

133-
Button("Fetch organization token") {
134-
Task {
135-
do {
136-
// Replace `<organization-id>` with a valid organization ID
137-
let token = try await client.getOrganizationToken(forId: "<organization-id>")
138-
print(token)
139-
} catch {
140-
print(error)
230+
Button("Fetch organization token claims") {
231+
Task { await vm.fetchOrganizationTokenClaims(for: DemoAuthConfig.organizationId) }
141232
}
233+
.disabled(!vm.isConfigured || !vm.isAuthenticated)
234+
235+
Button("Fetch access token claims in organization") {
236+
Task {
237+
await vm.fetchAccessTokenClaimsInOrg(
238+
resource: DemoAuthConfig.resourceToRequestTokenFor,
239+
organizationId: DemoAuthConfig.organizationId
240+
)
241+
}
242+
}
243+
.disabled(!vm.isConfigured || !vm.isAuthenticated)
244+
}
245+
246+
Section("Output") {
247+
Text(vm.output.isEmpty ? "(no output)" : vm.output)
248+
.font(.footnote)
249+
.textSelection(.enabled)
142250
}
143251
}
252+
.navigationTitle("Logto SwiftUI Demo")
144253
}
145254
}
146255
}
147-
148-
struct ContentView_Previews: PreviewProvider {
149-
static var previews: some View {
150-
ContentView()
151-
}
152-
}

0 commit comments

Comments
 (0)