6
6
//
7
7
8
8
import AuthenticationServices
9
+ import Awesome
9
10
import FollowAPI
10
11
import Kingfisher
11
12
import SwiftUI
@@ -26,7 +27,8 @@ class AuthenticationHandler: NSObject, ObservableObject, @unchecked Sendable {
26
27
func startAuthentication( ) {
27
28
guard let url = URL ( string: " https://app.follow.is/login?provider=github " ) else { return }
28
29
29
- webAuthSession = ASWebAuthenticationSession ( url: url, callbackURLScheme: " follow " ) { callbackURL, error in
30
+ webAuthSession = ASWebAuthenticationSession ( url: url, callbackURLScheme: " follow " ) {
31
+ callbackURL, error in
30
32
guard error == nil , let successURL = callbackURL else {
31
33
print ( " Authentication failed: \( error? . localizedDescription ?? " Unknown error " ) " )
32
34
return
@@ -42,7 +44,8 @@ class AuthenticationHandler: NSObject, ObservableObject, @unchecked Sendable {
42
44
43
45
private func handleCallback( url: URL ) {
44
46
guard let components = URLComponents ( url: url, resolvingAgainstBaseURL: false ) ,
45
- let queryItems = components. queryItems else { return }
47
+ let queryItems = components. queryItems
48
+ else { return }
46
49
47
50
if let token = queryItems. first ( where: { $0. name == " token " } ) ? . value {
48
51
Task {
@@ -63,7 +66,7 @@ class AuthenticationHandler: NSObject, ObservableObject, @unchecked Sendable {
63
66
}
64
67
}
65
68
}
66
-
69
+
67
70
private func setSessionToken( _ token: String ) {
68
71
do {
69
72
let data = try JSONEncoder ( ) . encode ( token)
@@ -94,7 +97,9 @@ class AuthenticationHandler: NSObject, ObservableObject, @unchecked Sendable {
94
97
private func loadSessionData( ) async {
95
98
if let sessionTokenData = KeychainWrapper . load ( forKey: " sessionToken " ) {
96
99
do {
97
- let sessionToken : String = try JSONDecoder ( ) . decode ( String . self, from: sessionTokenData)
100
+ let sessionToken : String = try JSONDecoder ( ) . decode (
101
+ String . self, from: sessionTokenData
102
+ )
98
103
NetworkManager . shared. setSessionToken ( sessionToken)
99
104
let csrfToken = try await authService. getCsrfToken ( ) . csrfToken
100
105
NetworkManager . shared. setCSRToken ( csrfToken)
@@ -109,8 +114,8 @@ class AuthenticationHandler: NSObject, ObservableObject, @unchecked Sendable {
109
114
110
115
func logout( ) {
111
116
isAuthenticated = false
112
- KeychainWrapper . delete ( forKey: " sessionData " )
113
- KeychainWrapper . delete ( forKey: " sessionToken " )
117
+ let _ = KeychainWrapper . delete ( forKey: " sessionData " )
118
+ let _ = KeychainWrapper . delete ( forKey: " sessionToken " )
114
119
NetworkManager . shared. clearTokens ( )
115
120
}
116
121
}
@@ -121,9 +126,29 @@ extension AuthenticationHandler: ASWebAuthenticationPresentationContextProviding
121
126
}
122
127
}
123
128
129
+ extension Color {
130
+ static let redGradientStart = Color (
131
+ light: . init( red: 252 / 255 , green: 165 / 255 , blue: 165 / 255 ) , // red-300
132
+ dark: . init( red: 239 / 255 , green: 68 / 255 , blue: 68 / 255 )
133
+ ) // red-500
134
+ static let orangeGradientEnd = Color (
135
+ light: . init( red: 253 / 255 , green: 186 / 255 , blue: 116 / 255 ) , // orange-300
136
+ dark: . init( red: 249 / 255 , green: 115 / 255 , blue: 22 / 255 )
137
+ ) // orange-500
138
+ }
139
+
124
140
struct LandingView : View {
125
141
@StateObject private var authHandler = AuthenticationHandler ( )
126
142
@State private var showMainView = false
143
+ @State private var logoOffset : CGFloat = 50
144
+ @State private var logoOpacity : CGFloat = 0
145
+ @State private var titleOffset : CGFloat = 50
146
+ @State private var titleOpacity : CGFloat = 0
147
+ @State private var subtitleOffset : CGFloat = 50
148
+ @State private var subtitleOpacity : CGFloat = 0
149
+ @State private var buttonsOffset : CGFloat = 50
150
+ @State private var buttonsOpacity : CGFloat = 0
151
+ @State private var animateGradient : Bool = false
127
152
128
153
var body : some View {
129
154
Group {
@@ -146,27 +171,109 @@ struct LandingView: View {
146
171
private var contentView : some View {
147
172
if let isAuthenticated = authHandler. isAuthenticated {
148
173
VStack {
149
- KFImage . url ( URL ( string: " https://github.com/RSSNext/follow/assets/41265413/c6c02ad5-cddc-46f5-8420-a47afe1c82fe " ) !)
150
- . resizable ( )
151
- . frame ( width: 80 , height: 80 )
174
+ Spacer ( )
175
+ HStack {
176
+ Image ( " FollowIcon " )
177
+ . resizable ( )
178
+ . frame ( width: 40 , height: 40 )
179
+ Text ( " Follow " )
180
+ . font ( . custom( " SNProVF-Bold " , size: 28 ) )
181
+ }
182
+ . offset ( y: logoOffset)
183
+ Spacer ( )
184
+ . frame ( height: 20 )
185
+ HStack ( spacing: 0 ) {
186
+ Text ( " Next-Gen " )
187
+ . background (
188
+ LinearGradient (
189
+ colors: [
190
+ . redGradientStart,
191
+ . orangeGradientEnd,
192
+ ] ,
193
+ startPoint: . leading,
194
+ endPoint: . trailing
195
+ )
196
+ . cornerRadius ( 8 )
197
+ . scaleEffect ( x: animateGradient ? 1 : 0 , y: 1 , anchor: . leading)
198
+ . animation ( . snappy( duration: 0.25 ) . delay ( 0.6 ) , value: animateGradient)
199
+ )
200
+ . onAppear {
201
+ animateGradient = true
202
+ }
203
+ Text ( " Information Browser " )
204
+ }
205
+ . font ( . custom( " SNProVF-Bold " , size: 20 ) )
206
+ . offset ( y: titleOffset)
207
+ . opacity ( titleOpacity)
152
208
209
+ Spacer ( )
153
210
if isAuthenticated {
154
211
Text ( " Redirecting... " )
212
+ . offset ( y: buttonsOffset)
213
+ . opacity ( buttonsOpacity)
155
214
} else {
156
- Button ( action: {
157
- Task {
158
- await authHandler. startAuthentication ( )
215
+ VStack ( spacing: 10 ) {
216
+ Button ( action: {
217
+ authHandler. startAuthentication ( )
218
+ } ) {
219
+ HStack {
220
+ Spacer ( )
221
+ Awesome . Brand. github. image
222
+ . foregroundColor ( . white)
223
+ Text ( " Continue with Github " )
224
+ . bold ( )
225
+ Spacer ( )
226
+ }
227
+ . padding ( )
228
+ . background ( Color . black)
229
+ . foregroundColor ( . white)
230
+ . cornerRadius ( 8 )
159
231
}
160
- } ) {
161
- Text ( " Continue with Github " )
232
+ Button ( action: {
233
+ Task {
234
+ // TODO: Sign in with Google
235
+ }
236
+ } ) {
237
+ HStack {
238
+ Spacer ( )
239
+ Awesome . Brand. google. image
240
+ . foregroundColor ( . white)
241
+ Text ( " Continue with Google " )
242
+ . bold ( )
243
+ Spacer ( )
244
+ }
162
245
. padding ( )
163
- . background ( Color . blue)
246
+ . background ( Color ( red : 59 / 255 , green : 130 / 255 , blue: 246 / 255 ) )
164
247
. foregroundColor ( . white)
165
248
. cornerRadius ( 8 )
249
+ }
166
250
}
251
+ . offset ( y: buttonsOffset)
252
+ . opacity ( buttonsOpacity)
167
253
}
168
254
}
169
255
. padding ( )
256
+ . onAppear {
257
+ withAnimation ( . snappy( duration: 0.5 ) . delay ( 0.0 ) ) {
258
+ logoOffset = 0
259
+ logoOpacity = 1
260
+ }
261
+
262
+ withAnimation ( . snappy( duration: 0.5 ) . delay ( 0.2 ) ) {
263
+ titleOffset = 0
264
+ titleOpacity = 1
265
+ }
266
+
267
+ withAnimation ( . snappy( duration: 0.5 ) . delay ( 0.4 ) ) {
268
+ subtitleOffset = 0
269
+ subtitleOpacity = 1
270
+ }
271
+
272
+ withAnimation ( . snappy( duration: 0.5 ) . delay ( 0.6 ) ) {
273
+ buttonsOffset = 0
274
+ buttonsOpacity = 1
275
+ }
276
+ }
170
277
} else {
171
278
ProgressView ( )
172
279
}
0 commit comments