@@ -9,144 +9,247 @@ import Logto
99import LogtoClient
1010import 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 \n response: \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