@leancodepl/cyberware-contract 10.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,721 @@
1
+ # @leancodepl/cyberware-contract
2
+
3
+ Creates type-safe contracts between a host app and a remote iframe app (e.g., Replit embed). Uses `postMessage` via
4
+ Penpal for secure cross-origin communication. Supports **Zod** for defining method params/returns and URL params with
5
+ runtime validation and inferred TypeScript types.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @leancodepl/cyberware-contract
11
+ ```
12
+
13
+ ```bash
14
+ yarn add @leancodepl/cyberware-contract
15
+ ```
16
+
17
+ ## API
18
+
19
+ ### `createContract(options)`
20
+
21
+ Creates a type-safe contract shared between host and remote. Use the same contract type on both sides to ensure method
22
+ signatures match.
23
+
24
+ **Options:**
25
+
26
+ - `contractVersion` - **Required.** Semver version of the contract (e.g. `"1.0.0"`). Host passes it via URL params;
27
+ remote verifies before connecting. When using the contract, this value is auto-injected into `useConnectToRemote`,
28
+ `useConnectToHost`, and `ConnectToHostProvider`—you don't pass it explicitly.
29
+ - `contractVersionRange` - **Required.** Semver range the host contract version must satisfy (e.g. `">=1.0.0 <2.0.0"`,
30
+ `"^2.0.0"`, `"~2.1.0"`). Remote checks `semver.satisfies(hostVersion, contractVersionRange)` before connecting.
31
+
32
+ **Returns:** Object with `useConnectToRemote`, `useConnectToHost`, `ConnectToHostProvider`, `useConnectToHostContext`,
33
+ and `getUrlParams`
34
+
35
+ ### `ConnectStatus`
36
+
37
+ Enum for connection state: `ConnectStatus.IDLE`, `ConnectStatus.CONNECTED`, `ConnectStatus.ERROR`,
38
+ `ConnectStatus.INCOMPATIBLE`. Used by `useConnectToRemote` and `useConnectToHost` (and context) return values. Check
39
+ `status === ConnectStatus.CONNECTED` before using `remote` or `host`; when `status === ConnectStatus.ERROR`, `error` is
40
+ set; when `status === ConnectStatus.INCOMPATIBLE` (host connect only), `hostVersion` and `remoteVersion` are set.
41
+
42
+ ### `connectToRemote(iframe, options)`
43
+
44
+ Connects host (parent) to remote (child iframe). Call from the host app with the iframe element.
45
+
46
+ **Parameters:**
47
+
48
+ - `iframe` - `HTMLIFrameElement` - The iframe element to connect to
49
+ - `options` - `ConnectToRemoteOptions<THost>` - Connection options
50
+
51
+ **Returns:** Penpal connection object with `promise` resolving to remote proxy and `destroy` method
52
+
53
+ ### `connectToHost(options)`
54
+
55
+ Connects remote (child iframe) to host (parent window). Call from the remote app.
56
+
57
+ **Parameters:**
58
+
59
+ - `options` - `ConnectToHostOptions<TRemote>` - Connection options
60
+
61
+ **Returns:** Penpal connection object with `promise` resolving to host proxy and `destroy` method
62
+
63
+ ### `useConnectToRemote(options)`
64
+
65
+ Connects host (parent) to remote (child iframe) and renders the iframe. Call from the host app.
66
+
67
+ **Parameters:**
68
+
69
+ - `options` - `UseConnectToRemoteOptions<THost, TParams>` - Connection options including `remoteUrl`, `iframeProps`
70
+ (with required `title`), `methods`, optional `params`, `allowedOrigins`. `contractVersion` is auto-injected from the
71
+ contract.
72
+
73
+ **Returns:** `UseConnectToRemoteResult<TRemote>` - Object with `iframe` element and connection state: `status`
74
+ (`ConnectStatus`), and when connected `remote` proxy, when error `error`
75
+
76
+ ### `useConnectToHost(options)`
77
+
78
+ Connects remote (child iframe) to host (parent window). Call from the remote app. Does nothing when not embedded in an
79
+ iframe.
80
+
81
+ **Parameters:**
82
+
83
+ - `options` - `UseConnectToHostOptions<TRemote>` - Connection options including `methods`, optional `allowedOrigins`.
84
+ `contractVersion` and `contractVersionRange` are auto-injected from the contract.
85
+
86
+ **Returns:** `UseConnectToHostResult<THost>` - Connection state: `status` (`ConnectStatus`), and when connected `host`
87
+ proxy, when error `error`, when incompatible `hostVersion` and `remoteVersion`
88
+
89
+ ### `createConnectToHostProvider()` / `ConnectToHostProvider`
90
+
91
+ `createConnectToHostProvider` creates a typed `ConnectToHostProvider` and `useConnectToHostContext` pair. Each contract
92
+ calls this internally; use `contract.ConnectToHostProvider` and `contract.useConnectToHostContext`.
93
+
94
+ **ConnectToHostProvider props:** `methods`, optional `allowedOrigins`, `children`. `contractVersion` and
95
+ `contractVersionRange` are auto-injected from the contract.
96
+
97
+ ### `buildRemoteUrl(baseUrl, params)`
98
+
99
+ Builds remote URL with query parameters. Merges params into the URL, preserving existing search params.
100
+
101
+ **Parameters:**
102
+
103
+ - `baseUrl` - `string` - Base URL without params
104
+ - `params` - `RemoteParamsWithContractVersion` (optional) - Params to merge (must include `contractVersion`)
105
+
106
+ **Returns:** `string` - Full URL with query string
107
+
108
+ ### `getUrlParams()`
109
+
110
+ Reads URL search params from the current location as a typed object. No validation is performed. Call from the remote
111
+ (iframe) to read params passed by the host. Always uses `location.search` in the browser.
112
+
113
+ **Returns:** `TParams` - Typed object of params (includes `contractVersion` when using a contract)
114
+
115
+ ### Zod helpers (optional)
116
+
117
+ When using Zod, you get inferred types and optional runtime validation:
118
+
119
+ - **`methodDef(def?)`** - Defines a method schema. `def` can have `params` (Zod object) and/or `returns` (Zod type). Use
120
+ for host and remote method signatures.
121
+ - **`InferMethodsFromSchema<T>`** - Infers `HostMethods` or `RemoteMethods` from a record of method-def schemas.
122
+ - **`InferParamsFromSchema<T>`** - Infers `RemoteParams` from a record of param schemas (e.g. `{ userId: z.string() }`).
123
+ - **`MethodType<S>`, `MethodParamsType<S>`, `MethodReturnType<S>`** - Per-method inferred types from a method-def
124
+ schema.
125
+ - **`mkZodContractSchema({ hostMethods, remoteMethods, remoteParams })`** - Builds a Zod schema for the full contract
126
+ (e.g. for validation or tooling). Types are still passed to `createContract` via the inferred types.
127
+
128
+ ## Usage Examples
129
+
130
+ ### Shared contract definition (TypeScript only)
131
+
132
+ Define the contract in a shared package or file used by both host and remote:
133
+
134
+ ```typescript
135
+ import { createContract } from "@leancodepl/cyberware-contract"
136
+
137
+ type HostMethods = {
138
+ navigateTo: (path: string) => Promise<void>
139
+ showNotification: (msg: string, type: "success" | "error") => Promise<void>
140
+ }
141
+
142
+ type RemoteMethods = {
143
+ getCurrentPath: () => Promise<string>
144
+ refresh: () => Promise<void>
145
+ }
146
+
147
+ type RemoteParams = { userId?: string; tenantId?: string }
148
+
149
+ export const contract = createContract<HostMethods, RemoteMethods, RemoteParams>({
150
+ contractVersion: "1.0.0",
151
+ contractVersionRange: ">=1.0.0 <2.0.0",
152
+ })
153
+ ```
154
+
155
+ ### Shared contract definition (with Zod)
156
+
157
+ Define method params/returns and URL params with Zod; types are inferred and you can reuse schemas for validation:
158
+
159
+ ```typescript
160
+ import { z } from "zod"
161
+ import {
162
+ createContract,
163
+ methodDef,
164
+ InferMethodsFromSchema,
165
+ InferParamsFromSchema,
166
+ type HostMethodsSchemaBase,
167
+ type RemoteMethodsSchemaBase,
168
+ type RemoteParamsSchemaBase,
169
+ } from "@leancodepl/cyberware-contract"
170
+
171
+ const NotificationTypeSchema = z.enum(["success", "error", "info"])
172
+
173
+ const HostMethods = {
174
+ navigateTo: methodDef({ params: z.object({ path: z.string() }) }),
175
+ showNotification: methodDef({
176
+ params: z.object({ message: z.string(), type: NotificationTypeSchema }),
177
+ }),
178
+ } satisfies HostMethodsSchemaBase
179
+
180
+ const RemoteMethods = {
181
+ getCurrentPath: methodDef({ returns: z.string() }),
182
+ refresh: methodDef(),
183
+ } satisfies RemoteMethodsSchemaBase
184
+
185
+ const RemoteParams = {
186
+ userId: z.string(),
187
+ tenantId: z.string().optional(),
188
+ } satisfies RemoteParamsSchemaBase
189
+
190
+ export type HostMethodsType = InferMethodsFromSchema<typeof HostMethods>
191
+ export type RemoteMethodsType = InferMethodsFromSchema<typeof RemoteMethods>
192
+ export type RemoteParamsType = InferParamsFromSchema<typeof RemoteParams>
193
+
194
+ export const contract = createContract<HostMethodsType, RemoteMethodsType, RemoteParamsType>({
195
+ contractVersion: "1.0.0",
196
+ contractVersionRange: ">=1.0.0 <2.0.0",
197
+ })
198
+ ```
199
+
200
+ ### Host app: embed remote iframe with React hook
201
+
202
+ ```tsx
203
+ import { ConnectStatus } from "@leancodepl/cyberware-contract"
204
+ import { contract } from "./contract"
205
+
206
+ function HostApp() {
207
+ const connection = contract.useConnectToRemote({
208
+ remoteUrl: "https://replit.example.com/app",
209
+ iframeProps: { title: "Remote app" },
210
+ methods: {
211
+ navigateTo: path => router.navigate(path),
212
+ showNotification: (msg, type) => messageApi[type](msg),
213
+ },
214
+ params: { userId: "123", tenantId: "acme" },
215
+ })
216
+
217
+ const handleSync = async () => {
218
+ if (connection.status === ConnectStatus.CONNECTED) await connection.remote.refresh()
219
+ }
220
+
221
+ return (
222
+ <div>
223
+ {connection.iframe}
224
+ {connection.status === ConnectStatus.CONNECTED && <button onClick={handleSync}>Sync</button>}
225
+ </div>
226
+ )
227
+ }
228
+ ```
229
+
230
+ ### Contract version checking
231
+
232
+ `contractVersion` and `contractVersionRange` are required in `createContract`. The host passes its version via URL
233
+ params to the iframe. The remote verifies compatibility with `semver.satisfies(hostVersion, contractVersionRange)`
234
+ before connecting. If versions are incompatible, connection state is `ConnectStatus.INCOMPATIBLE` with `hostVersion` and
235
+ `remoteVersion`. When using the contract, version values are auto-injected—no need to pass them to hooks or the
236
+ provider.
237
+
238
+ ### Remote app: using ConnectToHostProvider (Recommended)
239
+
240
+ Wrap the remote app with `ConnectToHostProvider` and use `useConnectToHostContext` in child components to access the
241
+ host connection without prop drilling:
242
+
243
+ ```tsx
244
+ import { ConnectStatus } from "@leancodepl/cyberware-contract"
245
+ import { contract } from "./contract"
246
+
247
+ function RemoteAppRoot() {
248
+ return (
249
+ <contract.ConnectToHostProvider
250
+ methods={{
251
+ getCurrentPath: () => Promise.resolve(location.pathname),
252
+ refresh: () => refetch(),
253
+ }}>
254
+ <RemoteApp />
255
+ </contract.ConnectToHostProvider>
256
+ )
257
+ }
258
+
259
+ function RemoteApp() {
260
+ const params = contract.getUrlParams()
261
+ const connection = contract.useConnectToHostContext()
262
+
263
+ const handleSave = async () => {
264
+ if (connection.status === ConnectStatus.CONNECTED)
265
+ await connection.host.showNotification("Settings saved", "success")
266
+ }
267
+
268
+ return (
269
+ <div>
270
+ <p>User: {params.userId}</p>
271
+ {connection.status === ConnectStatus.INCOMPATIBLE && (
272
+ <p>
273
+ Version mismatch: host {connection.hostVersion}, remote {connection.remoteVersion}
274
+ </p>
275
+ )}
276
+ {connection.status === ConnectStatus.CONNECTED && <button onClick={handleSave}>Save</button>}
277
+ </div>
278
+ )
279
+ }
280
+ ```
281
+
282
+ ### Remote app: connect to host and read params
283
+
284
+ ```tsx
285
+ import { ConnectStatus } from "@leancodepl/cyberware-contract"
286
+ import { contract } from "./contract"
287
+
288
+ function RemoteApp() {
289
+ const params = contract.getUrlParams()
290
+ const connection = contract.useConnectToHost({
291
+ methods: {
292
+ getCurrentPath: () => Promise.resolve(location.pathname),
293
+ refresh: () => refetch(),
294
+ },
295
+ })
296
+
297
+ const handleSave = async () => {
298
+ if (connection.status === ConnectStatus.CONNECTED)
299
+ await connection.host.showNotification("Settings saved", "success")
300
+ }
301
+
302
+ return (
303
+ <div>
304
+ <p>User: {params.userId}</p>
305
+ {connection.status === ConnectStatus.INCOMPATIBLE && (
306
+ <p>
307
+ Version mismatch: host {connection.hostVersion}, remote {connection.remoteVersion}
308
+ </p>
309
+ )}
310
+ {connection.status === ConnectStatus.CONNECTED && <button onClick={handleSave}>Save</button>}
311
+ </div>
312
+ )
313
+ }
314
+ ```
315
+
316
+ ### Imperative connection (without React)
317
+
318
+ Host side:
319
+
320
+ ```typescript
321
+ import { connectToRemote, buildRemoteUrl } from "@leancodepl/cyberware-contract"
322
+
323
+ const iframe = document.getElementById("remote") as HTMLIFrameElement
324
+ iframe.src = buildRemoteUrl("https://replit.example.com/app", { contractVersion: "1.0.0", userId: "123" })
325
+
326
+ const connection = connectToRemote(iframe, {
327
+ methods: {
328
+ navigateTo: path => router.navigate(path),
329
+ },
330
+ })
331
+ const remote = await connection.promise
332
+ await remote.refresh()
333
+ ```
334
+
335
+ Remote side:
336
+
337
+ ```typescript
338
+ import { connectToHost, getUrlParams } from "@leancodepl/cyberware-contract"
339
+
340
+ const params = getUrlParams<{ userId?: string }>()
341
+ const connection = connectToHost({
342
+ methods: {
343
+ getCurrentPath: () => Promise.resolve(location.pathname),
344
+ },
345
+ })
346
+ const host = await connection.promise
347
+ await host.showNotification("Ready", "success")
348
+ ```
349
+
350
+ ### Restricting allowed origins
351
+
352
+ ```typescript
353
+ connectToHost({
354
+ methods: { getCurrentPath: () => Promise.resolve(location.pathname) },
355
+ allowedOrigins: ["https://my-host-app.com", "https://localhost:3000"],
356
+ })
357
+ ```
358
+
359
+ ## React Host ↔ Flutter Remote
360
+
361
+ The contract can connect a **React host** to a **Flutter web remote** running inside an iframe. The Zod contract schema
362
+ is the single source of truth: TypeScript types are inferred for the React Host side, and
363
+ [`@leancodepl/cyberware-contract-generator-dart`](../cyberware-contract-generator-dart) generates Dart extension types
364
+ for the Flutter Remote side. The Flutter app uses
365
+ [`leancode_cyberware_contract_base`](https://github.com/nicepage/flutter_corelibrary/tree/main/packages/leancode_cyberware_contract_base)
366
+ for Cubit-based connection state management.
367
+
368
+ ### Example step-by-step setup
369
+
370
+ #### 1. Create the contract package
371
+
372
+ The contract package is both an **npm package** (for the React host) and a **Dart package** (for the Flutter remote). It
373
+ has both a `package.json` and a `pubspec.yaml`.
374
+
375
+ **Directory structure:**
376
+
377
+ ```
378
+ packages/my-contract/
379
+ ├── package.json
380
+ ├── pubspec.yaml
381
+ ├── cyberware-contract-generator-dart.config.js
382
+ ├── src/
383
+ │ └── lib/
384
+ │ ├── contract-schema.ts # Zod schema (source of truth)
385
+ │ ├── contract.ts # createContract call
386
+ │ └── types.ts # Re-exports inferred TS types
387
+ └── lib/
388
+ ├── my_contract.dart # Dart library barrel file
389
+ ├── generated/ # Generated Dart files (do not edit)
390
+ │ ├── contract.dart
391
+ │ ├── types.dart
392
+ │ └── connect_to_host.dart
393
+ └── contract/ # Hand-written Dart glue code
394
+ └── contract.dart # ConnectToHostCubit wrapper
395
+ ```
396
+
397
+ `pubspec.yaml` — declare the package as a Dart package that depends on `leancode_cyberware_contract_base`:
398
+
399
+ ```yaml
400
+ name: my_contract
401
+ publish_to: "none"
402
+ version: 1.0.0+1
403
+
404
+ environment:
405
+ sdk: ">=3.11.0 <4.0.0"
406
+
407
+ dependencies:
408
+ flutter:
409
+ sdk: flutter
410
+ bloc: ^9.0.0
411
+ flutter_bloc: ^9.0.0
412
+ leancode_cyberware_contract_base:
413
+ path: <path-to-leancode_cyberware_contract_base>
414
+ pub_semver: ^2.1.4
415
+ web: ^1.1.0
416
+ ```
417
+
418
+ `package.json` — include dev dependencies on `@leancodepl/cyberware-contract`,
419
+ `@leancodepl/cyberware-contract-generator-dart`, and `zod`:
420
+
421
+ ```json
422
+ {
423
+ "devDependencies": {
424
+ "@leancodepl/cyberware-contract": "*",
425
+ "@leancodepl/cyberware-contract-generator-dart": "*",
426
+ "zod": "^4.1.0"
427
+ }
428
+ }
429
+ ```
430
+
431
+ #### 2. Define the contract schema
432
+
433
+ Create the Zod schema in `contract-schema.ts`. This is the single source of truth for both TypeScript and Dart types:
434
+
435
+ ```typescript
436
+ import { z } from "zod"
437
+ import {
438
+ methodDef,
439
+ mkZodContractSchema,
440
+ type InferMethodsFromSchema,
441
+ type InferParamsFromSchema,
442
+ } from "@leancodepl/cyberware-contract"
443
+
444
+ const RemoteParamsSchema = {
445
+ userId: z.string(),
446
+ theme: z.enum(["light", "dark"]),
447
+ }
448
+
449
+ const HostMethodsSchema = {
450
+ navigateTo: methodDef({ params: z.object({ path: z.string() }) }),
451
+ showNotification: methodDef({
452
+ params: z.object({ message: z.string(), type: z.string().optional() }),
453
+ }),
454
+ getCurrentUserId: methodDef({ returns: z.string().nullable() }),
455
+ }
456
+
457
+ const RemoteMethodsSchema = {
458
+ onRouteChange: methodDef({ params: z.object({ path: z.string() }) }),
459
+ getCurrentPath: methodDef({ returns: z.string() }),
460
+ refresh: methodDef(),
461
+ }
462
+
463
+ export type HostMethods = InferMethodsFromSchema<typeof HostMethodsSchema>
464
+ export type RemoteMethods = InferMethodsFromSchema<typeof RemoteMethodsSchema>
465
+ export type RemoteParams = InferParamsFromSchema<typeof RemoteParamsSchema>
466
+
467
+ export const ContractSchema = mkZodContractSchema({
468
+ hostMethods: HostMethodsSchema,
469
+ remoteMethods: RemoteMethodsSchema,
470
+ remoteParams: RemoteParamsSchema,
471
+ })
472
+ ```
473
+
474
+ Create the contract instance in `contract.ts`:
475
+
476
+ ```typescript
477
+ import type { HostMethods, RemoteMethods, RemoteParams } from "./contract-schema"
478
+ import { createContract } from "@leancodepl/cyberware-contract"
479
+
480
+ export const { useConnectToRemote } = createContract<HostMethods, RemoteMethods, RemoteParams>({
481
+ contractVersion: "1.0.0",
482
+ contractVersionRange: ">=1.0.0",
483
+ })
484
+ ```
485
+
486
+ #### 3. Generate Dart types
487
+
488
+ Create `cyberware-contract-generator-dart.config.js` in the contract package root:
489
+
490
+ ```javascript
491
+ import { ContractSchema } from "./src/lib/contract-schema.ts"
492
+
493
+ /** @type {import("@leancodepl/cyberware-contract-generator-dart").CyberwareContractGeneratorDartConfig} */
494
+ const config = {
495
+ schema: ContractSchema,
496
+ outputDir: "./lib/generated",
497
+ }
498
+
499
+ export default config
500
+ ```
501
+
502
+ Run the generator:
503
+
504
+ ```bash
505
+ npx cyberware-contract-generator-dart
506
+ ```
507
+
508
+ This produces files in `lib/generated/`.
509
+
510
+ #### 4. Write Dart glue code
511
+
512
+ Create a `ConnectToHostCubit` wrapper that passes the contract version and wires the generated `connectToHost`:
513
+
514
+ ```dart
515
+ // lib/contract/contract.dart
516
+
517
+ import 'package:leancode_cyberware_contract_base/leancode_cyberware_contract_base.dart'
518
+ as base;
519
+ import '../generated/connect_to_host.dart';
520
+ import '../generated/types.dart';
521
+
522
+ class ConnectToHostCubitOptions {
523
+ const ConnectToHostCubitOptions({required this.methods});
524
+ final RemoteMethodsBase methods;
525
+ }
526
+
527
+ class ConnectToHostCubit
528
+ extends base.ConnectToHostCubit<RemoteMethodsBase, HostMethods> {
529
+ ConnectToHostCubit(ConnectToHostCubitOptions options)
530
+ : super(base.ConnectToHostCubitOptions(
531
+ connect: () => connectToHost(options.methods),
532
+ contractVersion: '1.0.0',
533
+ contractVersionRange: '>=1.0.0',
534
+ ));
535
+ }
536
+ ```
537
+
538
+ Create the Dart barrel file `lib/my_contract.dart`:
539
+
540
+ ```dart
541
+ export 'generated/connect_to_host.dart';
542
+ export 'generated/contract.dart';
543
+ export 'generated/types.dart';
544
+
545
+ export 'contract/contract.dart';
546
+ ```
547
+
548
+ #### 5. Set up the Flutter remote
549
+
550
+ Add the contract package and base package as dependencies in the Flutter app's `pubspec.yaml`:
551
+
552
+ ```yaml
553
+ dependencies:
554
+ flutter:
555
+ sdk: flutter
556
+ bloc: ^9.0.0
557
+ flutter_bloc: ^9.0.0
558
+ my_contract:
559
+ path: <path-to-contract-package>
560
+ leancode_cyberware_contract_base: 1.0.0
561
+ ```
562
+
563
+ Add the Penpal bridge script to `web/index.html` **before** the Flutter bootstrap script:
564
+
565
+ ```html
566
+ <head>
567
+ <!-- ... -->
568
+ <script src="assets/packages/leancode_cyberware_contract_base/assets/connect_to_host.js"></script>
569
+ </head>
570
+ <body>
571
+ <script src="flutter_bootstrap.js" async></script>
572
+ </body>
573
+ ```
574
+
575
+ Create class implementing the generated `RemoteMethodsBase`. This is where you define how the Flutter app responds to
576
+ calls from the host:
577
+
578
+ ```dart
579
+ import 'package:flutter/material.dart';
580
+ import 'package:my_contract/my_contract.dart';
581
+
582
+ class AppCyberwareMethods implements RemoteMethodsBase {
583
+ @override
584
+ Future<void> onRouteChange(RemoteOnRouteChangeParams params) {
585
+ debugPrint('Route changed: ${params.path}');
586
+ return Future.value();
587
+ }
588
+
589
+ @override
590
+ Future<RemoteGetCurrentPathResult> getCurrentPath() {
591
+ return Future.value('/current-path');
592
+ }
593
+
594
+ @override
595
+ Future<RemoteRefreshResult> refresh() {
596
+ return Future.value();
597
+ }
598
+ }
599
+ ```
600
+
601
+ Use `ConnectToHostCubit` with `BlocProvider` and pass the implementation:
602
+
603
+ ```dart
604
+ import 'package:flutter/material.dart';
605
+ import 'package:flutter_bloc/flutter_bloc.dart';
606
+ import 'package:my_contract/my_contract.dart';
607
+
608
+ class App extends StatelessWidget {
609
+ const App({super.key});
610
+
611
+ @override
612
+ Widget build(BuildContext context) {
613
+ final params = RemoteUrlParams();
614
+
615
+ return BlocProvider<ConnectToHostCubit>(
616
+ create: (_) => ConnectToHostCubit(
617
+ ConnectToHostCubitOptions(
618
+ methods: AppCyberwareMethods(),
619
+ ),
620
+ )..connect(),
621
+ child: MaterialApp(
622
+ home: HomePage(params: params),
623
+ ),
624
+ );
625
+ }
626
+ }
627
+
628
+ class HomePage extends StatelessWidget {
629
+ const HomePage({super.key, required this.params});
630
+
631
+ final RemoteUrlParams params;
632
+
633
+ @override
634
+ Widget build(BuildContext context) {
635
+ return Scaffold(
636
+ body: BlocBuilder<ConnectToHostCubit, ConnectToHostState>(
637
+ builder: (context, state) {
638
+ return switch (state) {
639
+ ConnectToHostStateIdle() => const Center(child: Text('Connecting...')),
640
+ ConnectToHostStateConnected(:final host) => Column(
641
+ children: [
642
+ Text('Connected! User: ${params.userId}'),
643
+ ElevatedButton(
644
+ onPressed: () => host.showNotification(
645
+ HostShowNotificationParams(message: 'Hello from Flutter!'),
646
+ ),
647
+ child: const Text('Show notification'),
648
+ ),
649
+ ],
650
+ ),
651
+ ConnectToHostStateError(:final error) =>
652
+ Center(child: Text('Error: $error')),
653
+ ConnectToHostStateIncompatible(:final hostVersion, :final remoteVersion) =>
654
+ Center(child: Text('Incompatible: host $hostVersion, remote $remoteVersion')),
655
+ };
656
+ },
657
+ ),
658
+ );
659
+ }
660
+ }
661
+ ```
662
+
663
+ #### 6. Embed the Flutter remote in the React host
664
+
665
+ In the React host app, use the contract's `useConnectToRemote` hook to embed the Flutter app in an iframe and implement
666
+ the host methods:
667
+
668
+ ```tsx
669
+ import { ConnectStatus } from "@leancodepl/cyberware-contract"
670
+ import { MyContract, MyContractTypes } from "@my-org/my-contract"
671
+
672
+ function FlutterAppPage() {
673
+ const methods: MyContractTypes.HostMethods = useMemo(
674
+ () => ({
675
+ navigateTo: ({ path }) => {
676
+ router.navigate(path)
677
+ return Promise.resolve()
678
+ },
679
+ showNotification: ({ message, type }) => {
680
+ notification.open({ content: message })
681
+ return Promise.resolve()
682
+ },
683
+ getCurrentUserId: async () => currentUser?.id ?? null,
684
+ }),
685
+ [],
686
+ )
687
+
688
+ const connection = MyContract.useConnectToRemote({
689
+ remoteUrl: "http://localhost:4220",
690
+ methods,
691
+ params: { userId: "demo-user", theme: "light" },
692
+ iframeProps: { title: "Flutter App" },
693
+ })
694
+
695
+ return (
696
+ <div>
697
+ {connection.iframe}
698
+ {connection.status === ConnectStatus.CONNECTED && (
699
+ <button onClick={() => connection.remote.refresh()}>Refresh</button>
700
+ )}
701
+ </div>
702
+ )
703
+ }
704
+ ```
705
+
706
+ ## Features
707
+
708
+ - **Type-safe contracts** - Shared TypeScript types ensure host and remote method signatures stay in sync
709
+ - **Zod support** - Optional Zod schemas via `methodDef`, `InferMethodsFromSchema`, and `InferParamsFromSchema` for
710
+ inferred types and reusable validation
711
+ - **Contract version checking** - Required `contractVersion` and `contractVersionRange` in `createContract`; remote
712
+ verifies host version satisfies the range via `semver.satisfies` before connecting
713
+ - **React hooks** - `useConnectToRemote` and `useConnectToHost` for declarative usage; connection state via
714
+ `ConnectStatus` and discriminated state (`status`, `remote`/`host`, `error`, `hostVersion`/`remoteVersion` when
715
+ incompatible)
716
+ - **URL params** - `buildRemoteUrl` and `getUrlParams` pass data from host to remote via query string
717
+ - **Origin validation** - Optional `allowedOrigins` restricts which domains can connect
718
+ - **Penpal-based** - Uses Penpal for reliable `postMessage` communication across iframe boundaries
719
+ - **Flutter remote support** - Generate Dart extension types from the Zod schema with
720
+ `@leancodepl/cyberware-contract-generator-dart`; use `leancode_cyberware_contract_base` for Cubit-based connection
721
+ state management in Flutter web apps