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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### main

* Internal: add test-only plumbing to forward a `--dart-define=MAPBOX_AGENT=<id>` value to the native platform channel at map creation, for use by this SDK's own test tooling. No public API changes; builds that do not pass this define are unaffected.
* Add `ModelSource` API, exposing the 3D model source (a collection of 3D models, each with its own position, orientation, and node/material overrides).
* Add `MapWidget.isOpaque` option to control whether the map is rendered as opaque or supports a transparent background. Set to `false` (together with a transparent style) to enable transparency on iOS; Android already supports this via `MapWidget.textureView` ([#415](https://github.com/mapbox/mapbox-maps-flutter/issues/415)).
* [iOS] Fix `updateSettings` on `CompassSettings`, `AttributionSettings`, `LogoSettings`, `IndoorSelectorSettings`, `ScaleBarSettings`, `GesturesSettings`, and `LocationComponentSettings` resetting omitted fields (position, margins, `enabled`, `scrollMode`, puck configuration) to defaults instead of preserving them, matching Android's partial-update behaviour.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,25 @@ class MapboxMapController(
result.error("MAX_REQUESTS_PER_HOST_ERROR", e.message, null)
}
}
"platform#setMapboxAgentId" -> {
// Forwards the compile-time-resolved MAPBOX_AGENT id (see
// `--dart-define=MAPBOX_AGENT=<id>`, only used by internal test tooling) into
// Common's agent-context registry, mirroring the `map#setMaxRequestsPerHost`
// call above.
//
// TODO: once a released `com.mapbox.common:common-android` artifact exposes
// `com.mapbox.common.MapboxAgentContextFactory`, replace this no-op with:
// com.mapbox.common.MapboxAgentContextFactory.getInstance().setMapboxAgentId(agentId)
// That class does not exist in any published Common release yet, so calling it
// here today would fail to compile; this handler is a placeholder that keeps the
// Dart-to-native wiring in place and ready to activate with a one-line change.
val agentId = call.argument<String>("agentId")
if (agentId.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "agentId cannot be null or empty", null)
} else {
result.success(null)
}
}
else -> {
result.notImplemented()
}
Expand Down
2 changes: 2 additions & 0 deletions example/integration_test/all_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import 'compass_test.dart' as compass_test;
import 'indoor_selector_test.dart' as indoor_selector_test;
import 'scale_bar_test.dart' as scale_bar_test;
import 'http_service_test.dart' as http_service_test;
import 'mapbox_agent_id_test.dart' as mapbox_agent_id_test;
import 'offline_test.dart' as offline_test;
import 'snapshotter/snapshotter_test.dart' as snapshotter_test;
import 'viewport_test.dart' as viewport_test;
Expand Down Expand Up @@ -80,6 +81,7 @@ void main() {
// offline_test
offline_test.main();
http_service_test.main();
mapbox_agent_id_test.main();

// style tests
style_test.main();
Expand Down
49 changes: 49 additions & 0 deletions example/integration_test/mapbox_agent_id_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'empty_map_widget.dart' as app;

// Verifies the wiring that forwards a compile-time `MAPBOX_AGENT` id
// (`--dart-define=MAPBOX_AGENT=<id>`) into the native platform channel when a
// real MapWidget is created. `MAPBOX_AGENT` is internal test tooling used to
// measure AI-coding-agent-driven traffic; it is never set for a normal app
// build.
//
// This test is self-verifying with respect to that define:
// - Run as `flutter test integration_test/mapbox_agent_id_test.dart` (no
// define, the default for this repo's test suite): confirms rendering a
// MapWidget still works with no agent id forwarded — i.e. this plumbing has
// zero effect on a normal build.
// - Run as `flutter test integration_test/mapbox_agent_id_test.dart
// --dart-define=MAPBOX_AGENT=claude-code`: additionally confirms the id is
// forwarded to native without the map failing to load.
//
// What this test does NOT verify: the resulting outbound `User-Agent` header
// actually carrying ` agent/claude-code`. This plugin has no Dart-visible API
// for inspecting raw outbound request headers, and forwarding the id into
// Mapbox Common's native agent-context registry is currently a documented
// no-op placeholder (see the `platform#setMapboxAgentId` handlers in
// MapboxMapController on Android/iOS) pending a Common release that exposes
// that registry as a dependency here. Once that ships and those handlers are
// wired up for real, header-level verification belongs in Common's own HTTP
// layer tests rather than being re-implemented in this plugin.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('MAPBOX_AGENT forwarding', () {
testWidgets(
'renders a MapWidget successfully whether or not MAPBOX_AGENT is set',
(widgetTester) async {
final mapFuture = app.main();
await widgetTester.pumpAndSettle();
final mapboxMap = await mapFuture;

// The map above must load regardless of kMapboxAgentIdForTesting's
// value; forwarding the id (or not) must never break map creation.
// Errors from the forwarding call itself are caught and logged rather
// than surfaced here (see _forwardMapboxAgentIdIfSet), so this only
// proves the forwarding attempt doesn't interfere with map creation —
// not that the native side actually received/applied the id.
expect(mapboxMap, isNotNull);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,28 @@ public final class MapboxMapController: NSObject, FlutterPlatformView {
HttpServiceFactory.setMaxRequestsPerHostForMax(UInt8(clamping: max))
result(nil)

case "platform#setMapboxAgentId":
// Forwards the compile-time-resolved MAPBOX_AGENT id (see
// `--dart-define=MAPBOX_AGENT=<id>`, only used by internal test tooling) into
// Common's agent-context registry, mirroring `map#setMaxRequestsPerHost` above.
//
// TODO: once a released Mapbox Common binary exposes a Swift/Obj-C binding for
// its agent-context registry, replace this no-op with the real forwarding call.
// That binding does not exist yet in any published release, so referencing it
// here today would fail to build; this handler is a placeholder that keeps the
// Dart-to-native wiring in place and ready to activate once it ships.
guard let arguments = methodCall.arguments as? [String: Any],
let agentId = arguments["agentId"] as? String, !agentId.isEmpty
else {
result(FlutterError(
code: "setMapboxAgentId",
message: "could not decode arguments",
details: nil
))
return
}
result(nil)

default:
result(FlutterMethodNotImplemented)
}
Expand Down
40 changes: 40 additions & 0 deletions lib/src/mapbox_maps_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@ part of mapbox_maps_flutter;

typedef OnPlatformViewCreatedCallback = void Function(int);

/// The AI-coding-agent identifier to forward to the native Mapbox Common SDK,
/// resolved at compile time via `--dart-define=MAPBOX_AGENT=<id>`.
///
/// Common resolves most agent detection itself, but on platforms where the
/// value can only be baked in at compile time (as is the case here, since
/// Dart's [String.fromEnvironment] is not a runtime OS environment variable
/// Common could read via `getenv`), a platform binding has to forward the
/// resolved id explicitly. This exists purely to support internal test
/// tooling — it is empty, and forwarding is skipped entirely, for every
/// normal app build, since none of them pass this define.
///
/// Exposed (rather than kept private) only so it can be exercised from this
/// package's tests; it is not part of the supported public API.
@internal
const String kMapboxAgentIdForTesting =
String.fromEnvironment('MAPBOX_AGENT', defaultValue: '');

/// The platform channel method used to forward [kMapboxAgentIdForTesting] to
/// the native side. Exposed for tests; not part of the supported public API.
@internal
const String kSetMapboxAgentIdMethod = 'platform#setMapboxAgentId';

class _MapboxMapsPlatform {
late final MethodChannel _channel = MethodChannel(
'plugins.flutter.io.${channelSuffix.toString()}',
Expand All @@ -13,6 +35,24 @@ class _MapboxMapsPlatform {
_MapboxMapsPlatform(
{required this.binaryMessenger, required this.channelSuffix}) {
_channel.setMethodCallHandler(_handleMethodCall);
_forwardMapboxAgentIdIfSet();
}

/// Forwards the compile-time [kMapboxAgentIdForTesting], if any, to the
/// native side as early as possible — before the platform view (and thus
/// the native map engine) is created — so it is set before this map
/// instance's requests go out. A no-op for any build that does not pass
/// `--dart-define=MAPBOX_AGENT=<id>`.
void _forwardMapboxAgentIdIfSet() {
if (kMapboxAgentIdForTesting.isEmpty) {
return;
}
_channel.invokeMethod(
kSetMapboxAgentIdMethod,
<String, dynamic>{'agentId': kMapboxAgentIdForTesting},
).catchError((Object error) {
print('Failed to forward MAPBOX_AGENT id to native platform: $error');
});
}

_MapboxMapsPlatform.instance(int channelSuffix)
Expand Down
50 changes: 50 additions & 0 deletions test/mapbox_maps_platform_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

const channelSuffix = 0;
final channel = MethodChannel('plugins.flutter.io.$channelSuffix');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;

late List<MethodCall> log;

setUp(() {
log = <MethodCall>[];
messenger.setMockMethodCallHandler(channel, (call) async {
log.add(call);
return null;
});
});

tearDown(() {
messenger.setMockMethodCallHandler(channel, null);
});

group('MAPBOX_AGENT forwarding', () {
test(
'MapboxMap.fromNativeController forwards the compile-time MAPBOX_AGENT '
'id to native only when --dart-define=MAPBOX_AGENT=<id> was passed to '
'this test run', () async {
// kMapboxAgentIdForTesting is a compile-time constant resolved from
// `--dart-define=MAPBOX_AGENT=<id>`. This test is self-verifying either
// way: normal `flutter test` runs (no define passed) exercise the
// no-forwarding branch, while `flutter test --dart-define=MAPBOX_AGENT=<id>`
// exercises the forwarding branch.
MapboxMap.fromNativeController(channelSuffix);
// The forwarding call is fire-and-forget, so let its microtask flush.
await pumpEventQueue();

if (kMapboxAgentIdForTesting.isEmpty) {
expect(log, isEmpty);
} else {
expect(log, hasLength(1));
expect(log.single.method, kSetMapboxAgentIdMethod);
expect(log.single.arguments, {'agentId': kMapboxAgentIdForTesting});
}
});
});
}