|
|
@@ -25,6 +25,19 @@ final class AuthenticationViewModel: ObservableObject {
|
|
|
private var authenticator: GoogleSignInAuthenticator {
|
|
|
return GoogleSignInAuthenticator(authViewModel: self)
|
|
|
}
|
|
|
+
|
|
|
+ /// The user's `auth_time` as found in `idToken`.
|
|
|
+ /// - note: If the user is logged out, then this will default to `nil`.
|
|
|
+ var authTime: Date? {
|
|
|
+ switch state {
|
|
|
+ case .signedIn(let user):
|
|
|
+ guard let idToken = user.idToken?.tokenString else { return nil }
|
|
|
+ return decodeAuthTime(fromJWT: idToken)
|
|
|
+ case .signedOut:
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/// The user-authorized scopes.
|
|
|
/// - note: If the user is logged out, then this will default to empty.
|
|
|
var authorizedScopes: [String] {
|
|
|
@@ -69,7 +82,48 @@ final class AuthenticationViewModel: ObservableObject {
|
|
|
@MainActor func addBirthdayReadScope(completion: @escaping () -> Void) {
|
|
|
authenticator.addBirthdayReadScope(completion: completion)
|
|
|
}
|
|
|
+
|
|
|
+ var formattedAuthTimeString: String? {
|
|
|
+ guard let date = authTime else { return nil }
|
|
|
+ let formatter = DateFormatter()
|
|
|
+ formatter.dateFormat = "MMM d, yyyy 'at' h:mm a"
|
|
|
+ return formatter.string(from: date)
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
+private extension AuthenticationViewModel {
|
|
|
+ func decodeAuthTime(fromJWT jwt: String) -> Date? {
|
|
|
+ let segments = jwt.components(separatedBy: ".")
|
|
|
+ guard let parts = decodeJWTSegment(segments[1]),
|
|
|
+ let authTimeInterval = parts["auth_time"] as? TimeInterval else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ return Date(timeIntervalSince1970: authTimeInterval)
|
|
|
+ }
|
|
|
+
|
|
|
+ func decodeJWTSegment(_ segment: String) -> [String: Any]? {
|
|
|
+ guard let segmentData = base64UrlDecode(segment),
|
|
|
+ let segmentJSON = try? JSONSerialization.jsonObject(with: segmentData, options: []),
|
|
|
+ let payload = segmentJSON as? [String: Any] else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ return payload
|
|
|
+ }
|
|
|
+
|
|
|
+ func base64UrlDecode(_ value: String) -> Data? {
|
|
|
+ var base64 = value
|
|
|
+ .replacingOccurrences(of: "-", with: "+")
|
|
|
+ .replacingOccurrences(of: "_", with: "/")
|
|
|
+
|
|
|
+ let length = Double(base64.lengthOfBytes(using: String.Encoding.utf8))
|
|
|
+ let requiredLength = 4 * ceil(length / 4.0)
|
|
|
+ let paddingLength = requiredLength - length
|
|
|
+ if paddingLength > 0 {
|
|
|
+ let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
|
|
|
+ base64 = base64 + padding
|
|
|
+ }
|
|
|
+ return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
extension AuthenticationViewModel {
|