From 913f400252d0ce6ba12598501517d90d57981de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 15 Jun 2026 12:09:21 +0200 Subject: [PATCH] feat(ios/macos): return serverAuthCode for offline access GIDSignInResult carries a one-time server auth code when a serverClientID is configured (GIDServerClientID). Surface it as `serverAuthCode` on the success result so a backend can exchange it for a refresh token and keep calling Google APIs after the short-lived access token expires. Without this, native clients only ever receive a ~1h access token, so any server-side data sync (e.g. Search Console) breaks once it expires. The interactive iOS + macOS paths both route through handleSignInResult, so both now include the code; silent restore omits it (no new code is issued). --- crate-ios/swift/google_auth_bridge.swift | 10 ++++++++-- src/index.ts | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crate-ios/swift/google_auth_bridge.swift b/crate-ios/swift/google_auth_bridge.swift index 0354c29..b142d65 100644 --- a/crate-ios/swift/google_auth_bridge.swift +++ b/crate-ios/swift/google_auth_bridge.swift @@ -39,7 +39,7 @@ private func emit( json.withCString { ptr in callback(ctx, ptr) } } -private func serializeUser(_ user: GIDGoogleUser) -> String { +private func serializeUser(_ user: GIDGoogleUser, serverAuthCode: String? = nil) -> String { // Hand-rolled JSON to avoid pulling Foundation's JSONSerialization // through a wrapper — the field set is fixed and small. func escape(_ s: String) -> String { @@ -85,6 +85,12 @@ private func serializeUser(_ user: GIDGoogleUser) -> String { let url = profile?.imageURL(withDimension: 256)?.absoluteString { fields.append("\"pictureUrl\":\"\(escape(url))\"") } + // Offline-access auth code (present when a serverClientID is configured). + // The server exchanges it for a refresh token so it can keep calling + // Google APIs after the access token expires. + if let code = serverAuthCode, !code.isEmpty { + fields.append("\"serverAuthCode\":\"\(escape(code))\"") + } return "{\(fields.joined(separator: ","))}" } @@ -215,7 +221,7 @@ private func handleSignInResult( emit(ctx, callback, failureJson("no-user-returned")) return } - emit(ctx, callback, serializeUser(user)) + emit(ctx, callback, serializeUser(user, serverAuthCode: result?.serverAuthCode)) } @_cdecl("perry_google_auth_bridge_silent_sign_in") diff --git a/src/index.ts b/src/index.ts index c6b97f5..9d1bfad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,6 +57,8 @@ export type GoogleSignInResult = idToken: string; /** OAuth access token; only present on flows where the user granted scope(s) that require it. */ accessToken?: string; + /** One-time server auth code (offline access); present when a serverClientID is configured. Exchange server-side for a refresh token. */ + serverAuthCode?: string; /** Stable Google user id (`sub`). */ userId: string; email: string;