Skip to content
Merged
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
18 changes: 16 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
# Changelog

## Unreleased

## 0.10.0 (2026-09-24)

Runtime pin 0.99.0, the first runtime with `host-pricing-v1`.

- **Host prices reach the map.** `SeatLayerPickerOptions.pricing.categories`
was declared but never sent, so a host that sells from its own catalogue
saw the chart's prices on the rail, the seat card and the cart. The picker
now boots the runtime with them (`init.config.pricing`) and
`SeatLayerPickerController.setPricing` changes them without a reload.
Needs a runtime advertising `host-pricing-v1`; an older one keeps the
chart's prices. Display only — the hold is still priced by SeatLayer.
- **Runtime pin 0.99.0** (was 0.84.1): the section overview, seat rendering
and row labels of the current web picker, the maintenance-pause message,
seats added after checkout listed in the runtime's own snapshot, host
pricing, and a seat card that no longer freezes 3D drag and pinch. The
bridge protocol range is unchanged (1–2).
- **A sold-out accessibility filter can still be turned off.** A provision
whose count reached zero went dark even while its switch was on, which
trapped the buyer holding the last space on a filtered map.
Expand Down
1 change: 1 addition & 0 deletions design/picker-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -2748,6 +2748,7 @@ from the wrong one.
| `hold-ownership-v1`, `hold-selection-v1` | who owns a hold, and holding a named selection |
| `checkout-handoff-v1`, `checkout-handoff-reject-v1` | the hold crossing to the host, and giving it back |
| `cart-line-remove-v1` | removing a cart line the selection cannot name |
| `host-pricing-v1` | the host's own category and tier prices (`pricing.categories`) on the rail, the seat card, the cart and the handoff, sent in `init.config.pricing` and changed live with `picker.setPricing`; a category the host leaves out keeps the chart's price, and an older runtime keeps the chart's prices for all |

A capability the runtime does not advertise is a feature that is **not offered**,
never a feature that fails.
Expand Down
36 changes: 35 additions & 1 deletion lib/src/picker/picker_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ typedef SeatLayerCheckoutCallback = FutureOr<void> Function(
typedef SeatLayerMoneyFormatter = String Function(
double amount, String currency);

/// The runtime capability that carries [SeatLayerPickerPricing.categories]
/// to the map: `init.config.pricing` and `cmd picker.setPricing`.
const String seatLayerHostPricingCapability = 'host-pricing-v1';

/// Host-supplied prices for one category and its tiers.
@immutable
class SeatLayerCategoryPricing {
Expand All @@ -25,14 +29,32 @@ class SeatLayerCategoryPricing {
this.tiers = const <String, double>{},
});

/// Price when the category has no tiers.
/// Price when the category has no tiers, and for a tier not in [tiers].
final double? base;

/// Price per tier id.
final Map<String, double> tiers;

/// The runtime's shape: a bare number for a flat price, else
/// `{ base, tiers }`; null when this entry sets nothing.
Object? toBridgeJson() {
if (tiers.isEmpty) return base;
return <String, Object?>{
if (base != null) 'base': base,
'tiers': Map<String, double>.of(tiers),
};
}
}

/// Host-supplied pricing and money formatting.
///
/// [categories] are the prices the buyer is shown — on the price rail, the
/// seat card, the cart and the checkout handoff — in place of the chart's own.
/// A ticketing host that charges from its own catalogue sets them so the map
/// says what checkout will charge; a category it leaves out keeps the chart's
/// price. Display only: the hold is still priced by SeatLayer. Needs a runtime
/// advertising [seatLayerHostPricingCapability]; an older one keeps the chart's
/// prices.
@immutable
class SeatLayerPickerPricing {
/// Creates a pricing override.
Expand All @@ -46,6 +68,16 @@ class SeatLayerPickerPricing {

/// How every amount in the native chrome is rendered.
final SeatLayerMoneyFormatter? formatter;

/// The runtime's `pricing` value, or null when no category sets a price.
Map<String, Object?>? toBridgeConfig() {
final prices = <String, Object?>{
for (final entry in categories.entries)
if (entry.value.toBridgeJson() != null)
entry.key: entry.value.toBridgeJson(),
};
return prices.isEmpty ? null : <String, Object?>{'prices': prices};
}
}

/// Visibility of the turnkey picker's native chrome.
Expand Down Expand Up @@ -401,6 +433,8 @@ class SeatLayerPickerOptions {
'hideEventDetails': hideEventDetails,
'panelCollapsed': panelInitiallyCollapsed,
'languages': languages.map((locale) => locale.toLanguageTag()).toList(),
if (pricing?.toBridgeConfig() != null)
'pricing': pricing!.toBridgeConfig(),
};
}

Expand Down
20 changes: 20 additions & 0 deletions lib/src/picker/seat_layer_picker_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,26 @@ class SeatLayerPickerController extends ValueNotifier<SeatLayerPickerState>
/// contain. Runtimes that do not report `floorMode` ignore it.
Future<void> showAllFloors() => setFloor(seatLayerAllFloors);

/// Whether the runtime takes host prices ([SeatLayerPickerPricing]).
bool get supportsHostPricing {
final bundle = mapController.bundleInfo;
return bundle != null &&
bundle.supportsCapability(seatLayerHostPricingCapability);
}

/// Show [pricing]'s category prices in place of the chart's without a
/// reload (null: the chart's own). Boot prices come from
/// [SeatLayerPickerOptions.pricing]; a runtime without
/// [seatLayerHostPricingCapability] keeps the chart's prices.
Future<void> setPricing(SeatLayerPickerPricing? pricing) {
if (!supportsHostPricing) return Future<void>.value();
return _mutation(
'picker.setPricing',
<String, Object?>{'pricing': pricing?.toBridgeConfig()},
SeatLayerPickerBusyAction.none,
);
}

Future<void> setColorblindSafe(bool enabled) => _mutation(
'picker.setColorblindSafe',
<String, Object?>{'on': enabled},
Expand Down
4 changes: 2 additions & 2 deletions lib/src/seat_layer_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import 'open_enums.dart';
import 'payloads.dart';

/// This SDK's version.
const String seatLayerSdkVersion = '0.9.1';
const String seatLayerSdkVersion = '0.10.0';

/// The SeatLayer renderer version this SDK release is pinned to.
const String seatLayerHostedWebVersion = '0.84.1';
const String seatLayerHostedWebVersion = '0.99.0';

/// Renderer version retained only for the example app's offline fixture.
///
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: >-
Flutter seat map SDK: a native seating chart and seat picker for iOS and
Android ticketing apps, with live availability, best available, seat holds,
3D views and secure checkout.
version: 0.9.1
version: 0.10.0
homepage: https://docs.seatlayer.io/buyer-sdk/flutter/
repository: https://github.com/seatlayer/seatlayer-flutter
issue_tracker: https://github.com/seatlayer/seatlayer-flutter/issues
Expand Down
2 changes: 1 addition & 1 deletion test/payloads_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'package:seatlayer/src/seat_layer_configuration.dart';

void main() {
test('hosted and fixture runtime versions remain explicitly pinned', () {
expect(seatLayerHostedWebVersion, '0.84.1');
expect(seatLayerHostedWebVersion, '0.99.0');
expect(seatLayerLegacyFixtureWebVersion, '0.68.0');
expect(
seatLayerMobilePageUrl,
Expand Down
79 changes: 79 additions & 0 deletions test/picker_host_pricing_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:seatlayer/seatlayer.dart';
import 'package:seatlayer/src/bridge/bridge_profile.dart';

void main() {
group('SeatLayerPickerPricing → host-pricing-v1', () {
test('a flat price travels as a number, a tiered one as { base, tiers }',
() {
const pricing = SeatLayerPickerPricing(
categories: <String, SeatLayerCategoryPricing>{
'gold': SeatLayerCategoryPricing(base: 486),
'vip': SeatLayerCategoryPricing(
base: 935,
tiers: <String, double>{'child': 673},
),
'kids': SeatLayerCategoryPricing(
tiers: <String, double>{'u12': 120},
),
'unset': SeatLayerCategoryPricing(),
},
);
expect(pricing.toBridgeConfig(), <String, Object?>{
'prices': <String, Object?>{
'gold': 486.0,
'vip': <String, Object?>{
'base': 935.0,
'tiers': <String, double>{'child': 673},
},
'kids': <String, Object?>{
'tiers': <String, double>{'u12': 120},
},
},
});
});

test('no category price means no pricing on the wire', () {
expect(const SeatLayerPickerPricing().toBridgeConfig(), isNull);
const formatterOnly = SeatLayerPickerOptions(
pricing: SeatLayerPickerPricing(formatter: _kroner),
);
expect(formatterOnly.toBridgeConfig().containsKey('pricing'), isFalse);
});

test('the picker boots the runtime with the prices in init.config', () {
const options = SeatLayerPickerOptions(
pricing: SeatLayerPickerPricing(
categories: <String, SeatLayerCategoryPricing>{
'gold': SeatLayerCategoryPricing(base: 486),
},
),
);
final payload = SeatLayerBridgeProfile.picker(
config: options.toBridgeConfig(),
).initPayload(SeatLayerConfiguration(event: 'ev_dkk', currency: 'DKK'));
final config = payload['config']! as Map<String, Object?>;
expect(config['currency'], 'DKK');
expect(config['pricing'], <String, Object?>{
'prices': <String, Object?>{'gold': 486.0},
});
});

test('equal prices build an equivalent profile, so nothing reboots', () {
SeatLayerBridgeProfile profile(double gold) =>
SeatLayerBridgeProfile.picker(
config: SeatLayerPickerOptions(
pricing: SeatLayerPickerPricing(
categories: <String, SeatLayerCategoryPricing>{
'gold': SeatLayerCategoryPricing(base: gold),
},
),
).toBridgeConfig(),
);
expect(profile(486).equivalentTo(profile(486)), isTrue);
expect(profile(486).equivalentTo(profile(500)), isFalse);
});
});
}

String _kroner(double amount, String currency) => '$currency $amount';
Loading