Sign members in through your backend and pass a Firebase custom token to the Flutter SDK.
Your backend authenticates the member and requests a Firebase custom token from
GOFA. Your Flutter app supplies that token through customTokenProvider.
Keep the GOFA client secret and any ClientToken exclusively on your backend.
GOFA enables member sign-in and supplies credentials separately for each environment.
{"customToken":"..."} to that authenticated app session,
with Cache-Control: no-store.The optional scoped ClientToken exchange is documented on the backend API page. It is unnecessary for the direct client ID/secret request.
The published integration versions are gofa_pose 2.2.19, gofa_lessons 1.2.12,
gofa_webview 2.1.5, gofa_core 1.1.6 and gofa_firebase 1.0.19.
Resolve packages through your authorized OnePub account and retain the resulting
lockfile. These examples target native Flutter Android/iOS applications.
For GofaWebView, use the HTTPS tenant host matching the configured client ID
and environment: https://<clientId>.uat.gofa.app for UAT or
https://<clientId>.gofa.app for production, retaining the experience's page
path. Member-token views reject a different tenant or environment. The SDK
also supports the matching canonical GOFA host (www.uat.gofa.app or
www.gofa.app); it does not send member tokens to arbitrary hosts.
Adapt the URL and member-session getter to your own backend. The session below is your app's member session. The app does not choose a member ID in this request.
import 'dart:convert';
import 'dart:io';
Future<String> Function() memberTokenProvider({
required Uri backendTokenUrl,
required Future<String> Function() getMemberSession,
}) => () async {
if (backendTokenUrl.scheme != 'https') {
throw StateError('HTTPS backend required');
}
final session = await getMemberSession();
if (session.isEmpty) throw StateError('Sign-in required');
final http = HttpClient()
..connectionTimeout = const Duration(seconds: 15);
try {
final req = await http.postUrl(backendTokenUrl);
req.headers.contentType = ContentType.json;
req.headers.set('Authorization', 'Bearer $session');
req.write('{}');
final res = await req.close().timeout(const Duration(seconds: 30));
final text = await utf8.decoder.bind(res).join()
.timeout(const Duration(seconds: 30));
if (res.statusCode != 200) throw StateError('GOFA sign-in unavailable');
final result = jsonDecode(text);
if (result is! Map || result['customToken'] is! String ||
(result['customToken'] as String).isEmpty) {
throw StateError('Missing sign-in token');
}
return result['customToken'] as String;
} finally {
http.close(force: true);
}
};Request a fresh token for each callback; do not store the custom token for reuse. Map backend session-expiry errors into your app's own sign-in experience. A network retry must recheck the current member session and request a fresh token.
Provide your existing backend URL, session getter and stable member ID below. The backend remains responsible for deriving and verifying that same ID.
import 'package:gofa_lessons/gofa_lessons.dart';
import 'package:gofa_webview/gofa_webview.dart';
final provider = memberTokenProvider(
backendTokenUrl: yourBackendTokenUrl,
getMemberSession: getCurrentMemberSession,
);
const environment = Environment.UAT; // Environment.PRD for production
const clientId = 'your-uat-client-id';
final memberId = existingStableMemberId;
final webview = WebViewSdkManager(environment: environment);
await webview.init(
environment: environment,
clientId: clientId,
clientUserId: memberId,
customTokenProvider: provider,
);
if (!webview.isInitialized) throw StateError('WebView not initialized');
final lessons = LessonSdkManager(isDemoApp: false);
await lessons.init(
environment: environment,
params: {
'clientId': clientId,
'clientUserId': memberId,
// Retain your other module-specific parameters.
},
customTokenProvider: provider,
);
if (!lessons.isInitialized) throw StateError('Lessons not initialized');Challenge (the gofa_pose package), Lessons and WebView use the same
customTokenProvider, environment, client ID and member ID. Any module may
initialize first; Challenge also works on its own. The SDK selects and shares
the Firebase app and member session internally.
import 'package:gofa_pose/gofa_pose.dart';
final pose = GofaPoseSdkManager(
// Keep your existing App Check, Crashlytics and device options.
);
await pose.init(
environment: environment,
clientId: clientId,
clientUserId: memberId,
customTokenProvider: provider,
);
if (!pose.isInitialized) throw StateError('Challenge not initialized');No manual Firebase-app handoff or fallback-account setting is needed in this member flow. Use the same provider across modules. A different tenant, member or environment is rejected instead of silently replacing the active session. After logout, module initialization cannot automatically sign the member back in; follow the explicit login renewal below. Verify camera access and result persistence in joint UAT.
Use the exact same external member ID already assigned to each member, including
case. GOFA constructs the Firebase UID as clientId|externalUserId. Do not send
that full prefixed UID as externalUserId, generate a new ID on each login, or
substitute an email address. Confirm existing records remain visible in UAT.
Firebase custom tokens expire after one hour and should be redeemed immediately. That is not a one-hour app-session limit. Firebase issues an ID token lasting about one hour and uses its refresh token to renew the signed-in session. See Firebase custom tokens and session management.
Use AuthManager.signOut() from gofa_firebase 1.0.19 or later, with
gofa_webview 2.1.5 or later. This immediately
blocks new SDK sign-ins, rejects pending custom-token results, waits for in-flight
authentication, signs out of Firebase and clears the manager's session state.
Automatic SDK listeners cannot sign the member back in. The SDK also blocks its
managed WebViews and waits for their separate GOFA web sessions to sign out.
The host app still blocks protected screens and controls navigation. The named
host functions below are integration hooks to implement in your app/backend,
not GOFA APIs.
import 'package:get_it/get_it.dart';
import 'package:gofa_firebase/gofa_firebase.dart';
Future<void> logoutMember(String previousSession) async {
// Host: synchronously block screens AND token-provider requests.
blockGofaScreensAndTokenRequests();
try {
await Future.wait<void>([
Future<void>.sync(() => GetIt.I<AuthManager>().signOut())
.whenComplete(clearLocalMemberSession), // Also runs if GOFA cleanup fails.
Future<void>.sync(() => invalidateMemberBackendSession(previousSession)),
], eagerError: false); // Attempt every step, even if another step fails.
} finally {
removeGofaScreensFromNavigation(); // Host UI; cleanup has settled.
}
}Pass the current backend session captured before clearing local state. The host gate must stop token-provider requests until a new verified login. Each cleanup step is attempted independently; an error still propagates after the steps finish. The host must handle failures, keep protected screens blocked and retry backend invalidation when offline. GOFA handles cleanup for SDK-managed WebViews, including GOFA sessions left by views closed earlier. It uses GOFA's web logout bridge and waits for confirmation; it does not wipe unrelated browser cookies or storage. A timeout or cleanup failure is an error, not a successful logout. Keep protected screens blocked and retry after resolving the failure.
Initialize WebViewSdkManager as shown above, even before opening a view, so it registers cleanup for the matching GOFA web origins after an app restart. Keep SDK views mounted behind the logout gate until signOut settles. The sample clears the local member session only after GOFA cleanup settles, even if cleanup fails; handle that failure and keep protected access blocked.
Your app owns clearing its local member session, calling its backend to invalidate that session, and removing GOFA screens from navigation. WebViews created outside the GOFA SDK remain your responsibility. Create fresh GOFA screens after a new verified login; do not reuse a previous member's view. Confirm logout and account switching on supported Android and iOS devices in joint UAT.
After logout succeeds and a new member login is verified, explicitly call
AuthManager.renewClientInformationAndSignIn with clientId, the same stable
clientUserId and the new session's customTokenProvider, or initialize a fresh
SDK lifecycle. Failed cleanup blocks sign-in until signOut() is retried and
succeeds; do not replace the AuthManager to bypass this protection. A plain
automatic signInWithUserId() does not resume after logout.
Local logout affects this device/session. Invalidating your own backend session prevents new custom-token requests through that session, but does not invalidate an already established GOFA Firebase session on another device.
GOFA can separately revoke Firebase refresh tokens for the member. This is an administrative operation; no partner-facing session-revocation endpoint is included in this migration. It affects the member's Firebase sessions across devices. Existing ID tokens can remain usable until expiry (about one hour) unless the receiving service also checks revocation. Immediate all-device invalidation requires a separately agreed, tested revocation-aware design. Rotating a client secret blocks future credential use; it does not itself revoke Firebase member sessions.
Verify the partner backend, exact member IDs, existing/new members, token refresh, logout and account switching, WebView, Lessons, Pose, supported Android/iOS devices and application rollout. GOFA's mock-backend tests do not replace acceptance of your backend and released app.