Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/site_shared/lib/developer_profiles.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2026 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Utilities for integrating with the Google Developer Profiles API,
/// also known as the Google Developer Program (GDP) API.
library;

export 'src/utils/developer_profiles_client.dart';
240 changes: 240 additions & 0 deletions packages/site_shared/lib/src/utils/developer_profiles_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
// Copyright 2026 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// A lightweight client for the Google Developer Profiles API
/// (`developers.googleapis.com`), also known as the
/// Google Developer Program (GDP) API.
library;

import 'dart:convert';

import 'package:http/http.dart' as http;

/// The OAuth 2.0 scope required to read and write
/// the signed-in user's Google Developer profile.
const String developerProfilesScope =
'https://www.googleapis.com/auth/devprofiles.full_control';

/// The Nomenclature node ID that represents the Flutter product.
///
/// Add it to a profile with [DeveloperProfilesClient.addInterests]
/// to mark the user as interested in Flutter.
const String flutterInterestNodeId = 'E-product-Flutter';

/// A deployment of the Google Developer Profiles API.
enum DeveloperProfilesEnvironment {
/// The staging deployment, used for development and testing.
staging('https://staging-developers.sandbox.googleapis.com'),

/// The production deployment.
production('https://developers.googleapis.com');

const DeveloperProfilesEnvironment(this.baseUrl);

/// The base URL of the API in this environment.
final String baseUrl;
}

/// A user's preference for receiving a category of email.
enum EmailPreference {
/// The user opted out of this category of email.
disabled('DISABLED'),

/// The user opted in to this category of email.
enabled('ENABLED');

const EmailPreference(this.value);

/// The value of this preference as expected by the API.
final String value;
}

/// The save state of an interest on a profile.
enum InterestState {
/// The user manually selected the interest.
selected('STATE_SELECTED'),

/// The interest was inferred from the user's previous behavior.
inferred('STATE_INFERRED');

const InterestState(this.value);

/// The value of this state as expected by the API.
final String value;
}

/// An error returned by the Google Developer Profiles API.
final class DeveloperProfilesException implements Exception {
/// Creates an exception for a failed request
/// with the specified HTTP [statusCode] and [message].
const DeveloperProfilesException(this.statusCode, this.message);

/// The HTTP status code of the failed response.
final int statusCode;

/// The error message returned by the API, if any.
final String message;

@override
String toString() => 'DeveloperProfilesException($statusCode): $message';
}

/// A client for the Google Developer Profiles API that
/// acts on behalf of the user who granted its access token.
///
/// The access token must be granted the [developerProfilesScope].
/// When running in the browser, obtain it with
/// Google Identity Services' OAuth 2.0 token client.
final class DeveloperProfilesClient {
/// Creates a client that authenticates requests with `accessToken`.
///
/// Requests go to the [environment] deployment, which
/// defaults to [DeveloperProfilesEnvironment.staging].
/// If specified, `apiKey` is sent as the `key` query parameter.
DeveloperProfilesClient({
required this._accessToken,
this.environment = .staging,
this._apiKey,
http.Client? httpClient,
}) : _httpClient = httpClient ?? http.Client();

/// The deployment of the API that requests are sent to.
final DeveloperProfilesEnvironment environment;

final String _accessToken;
final String? _apiKey;
final http.Client _httpClient;

/// Returns the resource name (`profiles/{id}`) of
/// the signed-in user's profile, or `null` if they don't have one.
Future<String?> hasProfile() async {
final response = await _send('GET', 'profile:has');
return switch (response) {
{'obfuscatedProfileId': final String id} when id.isNotEmpty =>
'profiles/$id',
_ => null,
};
}

/// Returns the signed-in user's profile as decoded JSON.
Future<Map<String, Object?>> getProfile() => _send('GET', 'profile:get');

/// Creates a profile for the signed-in user and
/// returns its resource name (`profiles/{id}`).
///
/// Use [eventEmail] and [newsletterEmail] to record the user's
/// consent to event and newsletter emails, and [originUri] to
/// attribute the profile creation to the page it happened on.
Future<String> createProfile({
EmailPreference? eventEmail,
EmailPreference? newsletterEmail,
String? originUri,
}) async {
final response = await _send(
'POST',
'profiles',
body: {
'eventEmail': ?eventEmail?.value,
'newsletterEmail': ?newsletterEmail?.value,
if (originUri != null) 'creationOrigin': {'originUri': originUri},
},
);
return switch (response) {
{'name': final String name} when name.isNotEmpty => name,
_ => throw const DeveloperProfilesException(
200,
'CreateProfile response did not include a profile name.',
),
};
}

/// Returns the resource name of the signed-in user's profile,
/// creating the profile with [createProfile] if it doesn't exist yet.
Future<({String name, bool created})> ensureProfile({
EmailPreference? eventEmail,
EmailPreference? newsletterEmail,
String? originUri,
}) async {
if (await hasProfile() case final existingName?) {
return (name: existingName, created: false);
}

final name = await createProfile(
eventEmail: eventEmail,
newsletterEmail: newsletterEmail,
originUri: originUri,
);
return (name: name, created: true);
}

/// Adds the Nomenclature [nodeIds] as interests of
/// the profile with resource name [profileName].
///
/// Interests that are already present on the profile are left as is.
/// At most 100 interests can be added in a single call.
Future<void> addInterests(
String profileName,
List<String> nodeIds, {
InterestState state = .selected,
}) async {
await _send(
'POST',
'$profileName/interests:batchUpdate',
body: {
'requests': [
for (final nodeId in nodeIds)
{
'interest': {
'name': '$profileName/interests/$nodeId',
'nodeId': nodeId,
'interestState': state.value,
},
},
],
},
);
}

/// Closes the underlying HTTP client.
void close() => _httpClient.close();

Future<Map<String, Object?>> _send(
String method,
String path, {
Map<String, Object?>? body,
}) async {
final baseUri = Uri.parse('${environment.baseUrl}/v1beta1/$path');
final uri = switch (_apiKey) {
final apiKey? => baseUri.replace(queryParameters: {'key': apiKey}),
null => baseUri,
};
final request = http.Request(method, uri)
..headers['Authorization'] = 'Bearer $_accessToken';
if (body != null) {
request
..headers['Content-Type'] = 'application/json'
..body = jsonEncode(body);
}

final response = await http.Response.fromStream(
await _httpClient.send(request),
);
final decoded = response.body.isEmpty
? const <String, Object?>{}
: jsonDecode(response.body);

if (response.statusCode < 200 || response.statusCode >= 300) {
final message = switch (decoded) {
{'error': {'message': final String message}} => message,
_ => response.reasonPhrase ?? 'Unknown error',
};
throw DeveloperProfilesException(response.statusCode, message);
}

return switch (decoded) {
final Map<String, Object?> json => json,
_ => const <String, Object?>{},
};
}
}
10 changes: 10 additions & 0 deletions sites/www/content/flutter-create-2026.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: Flutter Create 2026
bodyTags: interior flutter-create
description: >-
Flutter Create is Flutter's flagship livestream event on November 11, 2026.
Sign up to get notified when the livestream starts.
publishDate: "2026-10-13"
---

<FlutterCreatePage />
14 changes: 14 additions & 0 deletions sites/www/lib/main.client.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import 'package:flutter_website/src/components/pages/consultants_grid.dart'
deferred as _consultants_grid;
import 'package:flutter_website/src/components/pages/events_grid.dart'
deferred as _events_grid;
import 'package:flutter_website/src/components/pages/flutter_create_countdown.dart'
deferred as _flutter_create_countdown;
import 'package:flutter_website/src/components/pages/flutter_create_signup.dart'
deferred as _flutter_create_signup;
import 'package:flutter_website/src/components/pages/games_adaptive_media.dart'
deferred as _games_adaptive_media;
import 'package:flutter_website/src/components/pages/showcase_grid.dart'
Expand Down Expand Up @@ -138,6 +142,16 @@ ClientOptions get defaultClientOptions => ClientOptions(
),
loader: _events_grid.loadLibrary,
),
'flutter_create_countdown': ClientLoader(
(p) => _flutter_create_countdown.FlutterCreateCountdown(),
loader: _flutter_create_countdown.loadLibrary,
),
'flutter_create_signup': ClientLoader(
(p) => _flutter_create_signup.FlutterCreateSignup(
compact: p['compact'] as bool,
),
loader: _flutter_create_signup.loadLibrary,
),
'games_adaptive_media': ClientLoader(
(p) => _games_adaptive_media.GamesAdaptiveMedia(
mobileAssets: (p['mobileAssets'] as Map<String, Object?>)
Expand Down
2 changes: 2 additions & 0 deletions sites/www/lib/main.server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import 'src/pages/ecosystem_page.dart';
import 'src/pages/embedded_page.dart';
import 'src/pages/events_page.dart';
import 'src/pages/flip_page.dart';
import 'src/pages/flutter_create_page.dart';
import 'src/pages/games_page.dart';
import 'src/pages/google_integrations_page.dart';
import 'src/pages/home_page.dart';
Expand Down Expand Up @@ -126,6 +127,7 @@ void main() {
defineComponent('ShowcasePage', const ShowcasePage()),
defineComponent('BrandPage', const BrandPage()),
defineComponent('FlipPage', const FlipPage()),
defineComponent('FlutterCreatePage', const FlutterCreatePage()),
defineComponent('NewsPage', const NewsPage()),
defineComponent('WhyFlutterPage', const WhyFlutterPage()),
defineComponentWithAttrs('Image', Image.fromAttrs),
Expand Down
16 changes: 16 additions & 0 deletions sites/www/lib/main.server.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import 'package:flutter_website/src/components/pages/consultants_grid.dart'
as _consultants_grid;
import 'package:flutter_website/src/components/pages/events_grid.dart'
as _events_grid;
import 'package:flutter_website/src/components/pages/flutter_create_countdown.dart'
as _flutter_create_countdown;
import 'package:flutter_website/src/components/pages/flutter_create_signup.dart'
as _flutter_create_signup;
import 'package:flutter_website/src/components/pages/games_adaptive_media.dart'
as _games_adaptive_media;
import 'package:flutter_website/src/components/pages/showcase_grid.dart'
Expand Down Expand Up @@ -88,6 +92,15 @@ ServerOptions get defaultServerOptions => ServerOptions(
'events_grid',
params: __events_gridEventsGrid,
),
_flutter_create_countdown.FlutterCreateCountdown:
ClientTarget<_flutter_create_countdown.FlutterCreateCountdown>(
'flutter_create_countdown',
),
_flutter_create_signup.FlutterCreateSignup:
ClientTarget<_flutter_create_signup.FlutterCreateSignup>(
'flutter_create_signup',
params: __flutter_create_signupFlutterCreateSignup,
),
_games_adaptive_media.GamesAdaptiveMedia:
ClientTarget<_games_adaptive_media.GamesAdaptiveMedia>(
'games_adaptive_media',
Expand Down Expand Up @@ -179,6 +192,9 @@ Map<String, Object?> __events_gridEventsGrid(_events_grid.EventsGrid c) => {
'items': c.items.map((i) => i.toId()).toList(),
'past': c.past,
};
Map<String, Object?> __flutter_create_signupFlutterCreateSignup(
_flutter_create_signup.FlutterCreateSignup c,
) => {'compact': c.compact};
Map<String, Object?> __games_adaptive_mediaGamesAdaptiveMedia(
_games_adaptive_media.GamesAdaptiveMedia c,
) => {'mobileAssets': c.mobileAssets, 'desktopAssets': c.desktopAssets};
Expand Down
Loading
Loading