Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
nativephp-v3-to-v4-upgrade
nativephp-v3-to-v4-upgrade
nativephp/mobile
claude-code
laravel
android
ios
nativephp

Upgrade a NativePHP Mobile v3 app (Livewire/Volt/Flux in a web view) to v4 native SuperNative EDGE UI. Covers the composer/package moves the docs don't mention, the plugin rename, fonts and theme-push timing, the silent-failure layout traps, testing, and cutover. Activate when a project is on nativephp/mobile v3, when composer requires nativephp/native-ui, when native screens render as system font or blank/overlapping boxes, or when the user asks to upgrade/migrate to v4.

Install
php artisan boost:add-skill nativephp/mobile

Save this content to: .claude/skills/nativephp-v3-to-v4-upgrade/SKILL.md

# NativePHP Mobile v3 → v4 Upgrade

Battle-tested from migrating WhipRate mobile (18 screens, 21 Livewire components, full Flux Pro UI) to
100% native EDGE with 700+ passing tests. Follow the phases in order.

**The defining property of v4 debugging: almost every failure is SILENT.** Unsupported classes are
dropped, unsized nodes render nothing, a theme push that arrives too late leaves the system font in
place — none of it errors. When something looks wrong, assume "dropped silently" before "styled
wrong", and verify against source rather than reasoning about it.

## Phase 0 — Setup (get this wrong and everything fails)

### The package moved, and the marketplace lags

The UI component library was **renamed**, and this is not in most docs:

| v3 / older | v4 |
|---|---|
| `nativephp/native-ui` | `nativephp/mobile-ui` |
| `Nativephp\NativeUi\` | `Native\Mobile\UI\` |
| type `nativephp-ui-plugin` | type `nativephp-plugin` |

Three composer facts that block a naive `composer require`:

1. **`plugins.nativephp.com` may only publish a stale branch.** If `composer show nativephp/mobile-ui`
   lists one old dev branch, add a **vcs** repository for `https://github.com/NativePHP/mobile-ui`
   and list it FIRST — composer repos are canonical, so the first one providing the package wins.
2. **`nativephp/mobile` lives in the `mobile-air` repo** (`https://github.com/NativePHP/mobile-air`),
   not `NativePHP/mobile`.
3. **`mobile-ui` requires `nativephp/mobile: ^4.0`**, and mobile's `dev-main` carries no
   `branch-alias`, so the solver rejects it. Use an inline alias:
   `"nativephp/mobile": "dev-main as 4.0.99"`.

`mobile-ui` also raises the **iOS minimum to 18.2**. Check your deployment target before upgrading.

### Register the plugin — installing is not enough

`php artisan native:plugin:register nativephp/mobile-ui`, verify with `native:plugin:list`. The app's
`app/Providers/NativeServiceProvider::plugins()` imports the provider **by FQCN**, so the namespace
change has to be applied there too. Device builds then need a `native:run`, which you can run yourself
(`php artisan native:run <ios|android> <device> --no-tty -n` on a booted simulator or running emulator).

Without the UI plugin, `nativephp/mobile` ships the EDGE runtime but NOT the component library
(text/image/button/list/inputs, bottom-sheet, layout builders, `Theme`, `config/native-ui.php`, icon
tooling) — every element throws "Unknown native element type".

### Removal ordering for Livewire packages

Strip `VoltServiceProvider` (and any Livewire-touching provider) from `bootstrap/providers.php`
**before** `composer remove livewire/flux-pro livewire/flux livewire/volt livewire/livewire` —
`composer remove` triggers `package:discover`, which boots the app and fatals on missing classes.
Also delete the `flux-pro` entry from composer.json `repositories`.

### Trust source over docs

Package READMEs and bundled skill docs can be **ahead of the shipped code** — v4 docs referenced
`php artisan native:font` months before any package shipped it. The truth is
`vendor/nativephp/mobile-ui/src/`, `vendor/nativephp/mobile/src/Edge/`, and the renderers under
`resources/ios/` + `resources/android/`. If a documented command is missing, check which package it
belongs to before assuming your install is broken.

## Phase 1 — Understand (before writing any screen)

Produce three docs (parallel agents if orchestrating):
- **API reference from vendor source**: element catalog, `TailwindParser` whitelist, `NativeComponent`
  lifecycle, routing macros, layout builders, testing API.
- **Screen inventory**: per routed screen — state seeding (exact request/cache/DTO classes), every
  action, events, navigation, UI outline, Livewire mechanics needing translation, covering tests.
  Document non-routed embedded components (modals, permission managers) under the parent screens that
  must absorb them.
- **Design plan**: trace the real palette/typography out of the Tailwind CSS into a
  `config/native-ui.php` theme (light+dark for every role), plus tab/stack layout designs.

## Phase 2 — Scaffold (foundation before fan-out)

Order: publish native-ui config → fill theme+fonts → `native-ui:generate-icons` → write `NativeLayout`
classes (tabs / stack / form-stack / guest; `theme()` for all chrome colors, no hex) → move the screens
into `routes/mobile.php` as `Route::native()`/`Route::nativeGroup()` preserving every URI and route name,
and delete the Livewire routes from `routes/web.php` (the package loads `routes/mobile.php` itself, only in
native contexts) →
generate ONE component with `native:make` to prove path conventions → write a `conventions.md`
contract doc (namespaces, paths, layout assignments, theme tokens, gotchas) that all converters read.

Route order matters: static segments before `{param}` siblings (`/cars/create` before `/cars/{id}`).
Routes may reference not-yet-existing classes — registration is string-based, the app still boots.

### Fonts — the single most confusing part of the upgrade

The webview pulled fonts from a CDN. Native cannot. The whole custom-font path lives in
**`mobile-ui`**, not `nativephp/mobile`:

1. `php artisan native:font Inter --weights=400,600,700,900 --italic` downloads Google Fonts straight
   into `resources/fonts/`. **This command ships in `mobile-ui`.** Looking for it in
   `nativephp/mobile` is a dead end no matter how current that package is.
2. `CopyFontsCommand` (the plugin's `copy_assets` build hook) bundles `resources/fonts/*` into each
   native project at build time. Fonts only appear after a real `native:run`.
3. Register semantic aliases in `config/native-ui.php` `fonts` and use them in views (`font="headline"`).
   The **`default` alias is the app-wide font and supersedes the legacy `font-family` typography
   token** when both are set.
4. `--default` sets the app-wide alias — but it uses the LAST file it downloaded, which is often an
   italic. Check the value it wrote.

**Two traps that both present as "my font isn't working" (text renders as SF Pro / Roboto, upright):**

- **The theme push can fire before the bridge is reachable.** `Theme::pushToNative()` no-ops when
  `nativephp_call` isn't available, and `NativeUIServiceProvider` pushes from its own `boot()`. If it
  no-ops, the native side keeps its defaults — **alias map empty, font family "System"** — so every
  `font="…"` silently falls back. Re-push once the container is fully booted:
  ```php
  // app/Providers/NativeServiceProvider::boot()
  $this->app->booted(fn () => Theme::pushToNative());
  ```
- **A `font=` attribute needs a matching weight class.** Without `font-extrabold` / `font-semibold`
  alongside it, the face collapses to the regular weight.

**Colors working is NOT evidence the theme push works.** `bg-theme-*` classes are resolved to literal
hex per node in PHP, so colors look correct even when the bridge push never lands. That mismatch is
exactly what makes this read as a font-config problem instead of a theme-push one.

## Phase 3 — Shared partials before screens

Convert shared cards/skeletons/buttons/sheets to `resources/views/native/partials/` FIRST, with a
`PartialsTest.php` harness proving each renders. Document the host contract (required vars, action
names the host screen must implement, refs for tests) in conventions.md. Screen converters then
`@include('native.partials.X')` — parent public props propagate into includes automatically.

## Phase 4 — Convert screens (parallelizable, one agent per screen)

Strict file-ownership: each converter owns its component class, view, screen sub-partials, and test
file; NEVER edits routes, config, layouts, shared partials, or the data layer. Blockers get reported,
not fixed inline. Keep the entire data layer (Saloon requests, caches, DTOs, services) untouched.

### Livewire → EDGE translation

- **`native:model` is LIVE by default** — the opposite of Livewire 3. Add `.blur`/`.debounce.Xms`
  where the old code used deferred binding.
- **No built-in `validate()`**: manual `Validator` + `public array $errors` + `@nativeError`.
- **No query strings on native routes**: `?foo=1` becomes navigation data read via `$this->data('foo')`
  in `mount()`.
- `wire:init` → `#[Lazy]` + `placeholder()`; `x-intersect` infinite scroll → `@endReached` (throttle
  it); pull-to-refresh → `refreshable`/`@refresh`; `wire:poll` → `#[Poll(ms)]`; flux modals →
  `native:bottom-sheet`/`native:modal`; teleported nav actions → per-screen
  `navigationOptions`/`bottomBar`; `hideTabBar` → `$hidesTabBar`.
- **Traits calling Livewire's `$this->redirect()`**: add a screen-local `redirect()` proxy forwarding
  to `navigate()` rather than editing the shared trait mid-migration; port it properly in cleanup.
- **Icons**: `App\Icons` enums via `:ios`/`:android`; a raw SF Symbol / Material name string is
  accepted when an enum case is missing.
- **Event directives interpolate**: `@tap="{{ $action }}({{ $id }})"` works — enables parametrizable
  partials.
- Missing native capability (e.g. HEIC conversion the webview did)? Flag for a **plugin** decision —
  never fall back to the web view.

### Layout traps that render NOTHING and never error

These cost the most time. All verified from renderer source.

1. **An empty `<native:column>` paints nothing.** `NativeUIColumnRenderer` opens with
   `if node.children.isEmpty { Color.clear }` — background, width and height all discarded. Use
   **`<native:rect />`** (self-closing) for any solid fill or scrim band; it renders
   `Rectangle().fill(...)` from `style.bg_color`. `native:circle` / `native:line` are the equivalents
   for other shapes.
2. **Absolute children are ANCHORED, never stretched.** `NativeUIStackLayout` and
   `FlexContainer.placeAbsolute` place a child at its own measured size. Opposing insets do NOT imply
   a size the way they do in CSS, so `absolute inset-0` yields an intrinsic-sized child at top-left —
   nothing at all for a rect. To fill a stack: `absolute top-0 left-0 w-full h-full`.
3. **A zero inset means "no anchor on that edge."** The packed node struct has no spare byte to
   distinguish an unset edge from an explicit `0`, so `bottom-0` pins to the **TOP** and `right-0`
   anchors **LEFT**. Use the smallest real value (`bottom-px`) when you want flush. Negative insets
   deliberately overhang (`-right-8` bleed).
4. **Aspect ratio goes on the STACK, not the image.** A remote image's natural size is tiny, so
   `<native:stack class="w-full"><native:image class="aspect-[3/4]">` leaves the stack collapsed and
   the image overflows every sibling below it. Put `aspect-[3/4]` on the stack and give the image
   `w-full h-full`.
5. **Unsupported Tailwind classes are dropped in silence.** Verify before assuming:
   `TailwindParser::parse('the-class')` returning `[]` means unsupported. Known gaps:
   `scale-*`, per-corner radii (`rounded-t-xl` — only uniform keys parse), `max-w-*` / `min-w-*`.
   Gradients (`bg-gradient-to-*` / `bg-linear-to-*` with `from`/`via`/`to`) and `inset-*` ARE
   supported as of 2026-07.
6. **`leading-*` maps to SwiftUI `.lineSpacing`** — space BETWEEN lines. It is a no-op on single-line
   text, and iOS cannot tighten below the font's natural line height. Adjust padding instead.
7. **Image `src` must be an absolute on-device path** — `public_path('img/x.png')`. A relative
   `img/x.png` is handed to AsyncImage/Coil, which can't resolve it, and renders blank.

### Tests are the safety net

Port every legacy Livewire test to `Native\Mobile\Testing\Native::test()`/`Native::visit()` with
`FakeBridge` + Saloon `MockClient`. Cover happy/failure/weird paths (401→login redirect, offline,
empty feeds). Add `ref` attributes to interactive elements for test targeting.

Wire-format facts that make assertions land first time:
- The test harness renders through `createElement`, a **different code path from the device's
  streaming collector**. A prop can work in tests and be missing on device, or vice versa.
- Fills live at **`style.bg_color`** as `#AARRGGBB` — there is no `props.bg`.
- `uppercase` is applied by the renderer, so the `text` prop keeps its **original case**.
- `layout.position` is `[top, right, bottom, left]`; `layout.position_type === 1` means absolute.
- `assertElement($type, $matcher)` + `tree()` for structural assertions; guard `position` with an
  `is_array`/`count` check, since not every node carries one.

## Phase 5 — Integrate + cutover

1. Integrator: fix reported blockers, verify every route resolves (`NativeRouter->resolve()` returns
   `['class','layout','params']`), run the full native suite plus untouched data-layer suites, and
   grep native views for hex literals / non-whitelist classes / leftover `wire:`/`flux:` syntax.
2. Only once green: delete `app/Livewire`, `resources/views/livewire`, `resources/views/flux`, legacy
   `resources/views/components`, and old Livewire test dirs. Rewrite generic smoke tests
   (`assertSeeLivewire` → route-resolution or `Native::visit()->assertScreen()`).
3. `composer dump-autoload`, full suite, `vendor/bin/pint --dirty`, `php artisan native:validate`.
4. Rebuild with `native:run` on a simulator or emulator as the final check — plugin registration, plugin
   Swift/Kotlin sources AND bundled fonts all compile in at build time.

## Patching the framework during an upgrade

Local path checkouts of `mobile` / `mobile-ui` are common on this path. If you add a style utility,
it must travel **three layers**, and the collector has **four** call sites — missing one produces the
classic "works in tests, missing on device" split:

- `mobile/src/Edge/TailwindParser.php` — class → attrs. A feature spanning several classes (like
  gradients) needs a **deep-merge** branch in `parse()`, as `dark` has; a flat `array_merge` lets the
  last class clobber the earlier ones.
- `mobile/src/Edge/NativeElementCollector.php` — attrs → wire. Sites: `openStreaming`,
  `leafStreaming` (each with a builtin and a plugin branch), and **`createElement`**, which is what
  the test harness uses.
- `NodeStyleModifier.swift` / `NodeModifiers.kt` — the single choke point where every node's
  background is painted; wiring there makes a utility work on any element.

The node is a **packed 160-byte binary struct** with fixed offsets, so the style block cannot take
variable-length data — ride the **props bag** instead (`dark_bg_color` does). Unknown prop keys are
fine: the encoder falls back to an inline string for anything outside the interned `PropKey` table.

## Orchestration notes (multi-agent)

- The `conventions.md` contract doc is what makes parallel conversion safe — write it before fan-out,
  append the partials contract to it.
- Effort scaling: complex screens (multi-step forms, camera, geolocation) get high effort; simple
  lists don't.
- Workflow `args` may arrive as a JSON string — guard with
  `typeof args === 'string' ? JSON.parse(args) : args`.
- Cache-resume: fix a workflow script bug and resume with the same run ID; completed agents replay free.

NativePHP Mobile v3 → v4 Upgrade

Battle-tested from migrating WhipRate mobile (18 screens, 21 Livewire components, full Flux Pro UI) to 100% native EDGE with 700+ passing tests. Follow the phases in order.

The defining property of v4 debugging: almost every failure is SILENT. Unsupported classes are dropped, unsized nodes render nothing, a theme push that arrives too late leaves the system font in place — none of it errors. When something looks wrong, assume "dropped silently" before "styled wrong", and verify against source rather than reasoning about it.

Phase 0 — Setup (get this wrong and everything fails)

The package moved, and the marketplace lags

The UI component library was renamed, and this is not in most docs:

v3 / older v4
nativephp/native-ui nativephp/mobile-ui
Nativephp\NativeUi\ Native\Mobile\UI\
type nativephp-ui-plugin type nativephp-plugin

Three composer facts that block a naive composer require:

  1. plugins.nativephp.com may only publish a stale branch. If composer show nativephp/mobile-ui lists one old dev branch, add a vcs repository for https://github.com/NativePHP/mobile-ui and list it FIRST — composer repos are canonical, so the first one providing the package wins.
  2. nativephp/mobile lives in the mobile-air repo (https://github.com/NativePHP/mobile-air), not NativePHP/mobile.
  3. mobile-ui requires nativephp/mobile: ^4.0, and mobile's dev-main carries no branch-alias, so the solver rejects it. Use an inline alias: "nativephp/mobile": "dev-main as 4.0.99".

mobile-ui also raises the iOS minimum to 18.2. Check your deployment target before upgrading.

Register the plugin — installing is not enough

php artisan native:plugin:register nativephp/mobile-ui, verify with native:plugin:list. The app's app/Providers/NativeServiceProvider::plugins() imports the provider by FQCN, so the namespace change has to be applied there too. Device builds then need a native:run, which you can run yourself (php artisan native:run <ios|android> <device> --no-tty -n on a booted simulator or running emulator).

Without the UI plugin, nativephp/mobile ships the EDGE runtime but NOT the component library (text/image/button/list/inputs, bottom-sheet, layout builders, Theme, config/native-ui.php, icon tooling) — every element throws "Unknown native element type".

Removal ordering for Livewire packages

Strip VoltServiceProvider (and any Livewire-touching provider) from bootstrap/providers.php before composer remove livewire/flux-pro livewire/flux livewire/volt livewire/livewire — composer remove triggers package:discover, which boots the app and fatals on missing classes. Also delete the flux-pro entry from composer.json repositories.

Trust source over docs

Package READMEs and bundled skill docs can be ahead of the shipped code — v4 docs referenced php artisan native:font months before any package shipped it. The truth is vendor/nativephp/mobile-ui/src/, vendor/nativephp/mobile/src/Edge/, and the renderers under resources/ios/ + resources/android/. If a documented command is missing, check which package it belongs to before assuming your install is broken.

Phase 1 — Understand (before writing any screen)

Produce three docs (parallel agents if orchestrating):

  • API reference from vendor source: element catalog, TailwindParser whitelist, NativeComponent lifecycle, routing macros, layout builders, testing API.
  • Screen inventory: per routed screen — state seeding (exact request/cache/DTO classes), every action, events, navigation, UI outline, Livewire mechanics needing translation, covering tests. Document non-routed embedded components (modals, permission managers) under the parent screens that must absorb them.
  • Design plan: trace the real palette/typography out of the Tailwind CSS into a config/native-ui.php theme (light+dark for every role), plus tab/stack layout designs.

Phase 2 — Scaffold (foundation before fan-out)

Order: publish native-ui config → fill theme+fonts → native-ui:generate-icons → write NativeLayout classes (tabs / stack / form-stack / guest; theme() for all chrome colors, no hex) → move the screens into routes/mobile.php as Route::native()/Route::nativeGroup() preserving every URI and route name, and delete the Livewire routes from routes/web.php (the package loads routes/mobile.php itself, only in native contexts) → generate ONE component with native:make to prove path conventions → write a conventions.md contract doc (namespaces, paths, layout assignments, theme tokens, gotchas) that all converters read.

Route order matters: static segments before {param} siblings (/cars/create before /cars/{id}). Routes may reference not-yet-existing classes — registration is string-based, the app still boots.

Fonts — the single most confusing part of the upgrade

The webview pulled fonts from a CDN. Native cannot. The whole custom-font path lives in mobile-ui, not nativephp/mobile:

  1. php artisan native:font Inter --weights=400,600,700,900 --italic downloads Google Fonts straight into resources/fonts/. This command ships in mobile-ui. Looking for it in nativephp/mobile is a dead end no matter how current that package is.
  2. CopyFontsCommand (the plugin's copy_assets build hook) bundles resources/fonts/* into each native project at build time. Fonts only appear after a real native:run.
  3. Register semantic aliases in config/native-ui.php fonts and use them in views (font="headline"). The default alias is the app-wide font and supersedes the legacy font-family typography token when both are set.
  4. --default sets the app-wide alias — but it uses the LAST file it downloaded, which is often an italic. Check the value it wrote.

Two traps that both present as "my font isn't working" (text renders as SF Pro / Roboto, upright):

  • The theme push can fire before the bridge is reachable. Theme::pushToNative() no-ops when nativephp_call isn't available, and NativeUIServiceProvider pushes from its own boot(). If it no-ops, the native side keeps its defaults — alias map empty, font family "System" — so every font="…" silently falls back. Re-push once the container is fully booted:
    // app/Providers/NativeServiceProvider::boot()
    $this->app->booted(fn () => Theme::pushToNative());
    
  • A font= attribute needs a matching weight class. Without font-extrabold / font-semibold alongside it, the face collapses to the regular weight.

Colors working is NOT evidence the theme push works. bg-theme-* classes are resolved to literal hex per node in PHP, so colors look correct even when the bridge push never lands. That mismatch is exactly what makes this read as a font-config problem instead of a theme-push one.

Phase 3 — Shared partials before screens

Convert shared cards/skeletons/buttons/sheets to resources/views/native/partials/ FIRST, with a PartialsTest.php harness proving each renders. Document the host contract (required vars, action names the host screen must implement, refs for tests) in conventions.md. Screen converters then @include('native.partials.X') — parent public props propagate into includes automatically.

Phase 4 — Convert screens (parallelizable, one agent per screen)

Strict file-ownership: each converter owns its component class, view, screen sub-partials, and test file; NEVER edits routes, config, layouts, shared partials, or the data layer. Blockers get reported, not fixed inline. Keep the entire data layer (Saloon requests, caches, DTOs, services) untouched.

Livewire → EDGE translation

  • native:model is LIVE by default — the opposite of Livewire 3. Add .blur/.debounce.Xms where the old code used deferred binding.
  • No built-in validate(): manual Validator + public array $errors + @nativeError.
  • No query strings on native routes: ?foo=1 becomes navigation data read via $this->data('foo') in mount().
  • wire:init → #[Lazy] + placeholder(); x-intersect infinite scroll → @endReached (throttle it); pull-to-refresh → refreshable/@refresh; wire:poll → #[Poll(ms)]; flux modals → native:bottom-sheet/native:modal; teleported nav actions → per-screen navigationOptions/bottomBar; hideTabBar → $hidesTabBar.
  • Traits calling Livewire's $this->redirect(): add a screen-local redirect() proxy forwarding to navigate() rather than editing the shared trait mid-migration; port it properly in cleanup.
  • Icons: App\Icons enums via :ios/:android; a raw SF Symbol / Material name string is accepted when an enum case is missing.
  • Event directives interpolate: @tap="{{ $action }}({{ $id }})" works — enables parametrizable partials.
  • Missing native capability (e.g. HEIC conversion the webview did)? Flag for a plugin decision — never fall back to the web view.

Layout traps that render NOTHING and never error

These cost the most time. All verified from renderer source.

  1. An empty <native:column> paints nothing. NativeUIColumnRenderer opens with if node.children.isEmpty { Color.clear } — background, width and height all discarded. Use <native:rect /> (self-closing) for any solid fill or scrim band; it renders Rectangle().fill(...) from style.bg_color. native:circle / native:line are the equivalents for other shapes.
  2. Absolute children are ANCHORED, never stretched. NativeUIStackLayout and FlexContainer.placeAbsolute place a child at its own measured size. Opposing insets do NOT imply a size the way they do in CSS, so absolute inset-0 yields an intrinsic-sized child at top-left — nothing at all for a rect. To fill a stack: absolute top-0 left-0 w-full h-full.
  3. A zero inset means "no anchor on that edge." The packed node struct has no spare byte to distinguish an unset edge from an explicit 0, so bottom-0 pins to the TOP and right-0 anchors LEFT. Use the smallest real value (bottom-px) when you want flush. Negative insets deliberately overhang (-right-8 bleed).
  4. Aspect ratio goes on the STACK, not the image. A remote image's natural size is tiny, so <native:stack class="w-full"><native:image class="aspect-[3/4]"> leaves the stack collapsed and the image overflows every sibling below it. Put aspect-[3/4] on the stack and give the image w-full h-full.
  5. Unsupported Tailwind classes are dropped in silence. Verify before assuming: TailwindParser::parse('the-class') returning [] means unsupported. Known gaps: scale-*, per-corner radii (rounded-t-xl — only uniform keys parse), max-w-* / min-w-*. Gradients (bg-gradient-to-* / bg-linear-to-* with from/via/to) and inset-* ARE supported as of 2026-07.
  6. leading-* maps to SwiftUI .lineSpacing — space BETWEEN lines. It is a no-op on single-line text, and iOS cannot tighten below the font's natural line height. Adjust padding instead.
  7. Image src must be an absolute on-device path — public_path('img/x.png'). A relative img/x.png is handed to AsyncImage/Coil, which can't resolve it, and renders blank.

Tests are the safety net

Port every legacy Livewire test to Native\Mobile\Testing\Native::test()/Native::visit() with FakeBridge + Saloon MockClient. Cover happy/failure/weird paths (401→login redirect, offline, empty feeds). Add ref attributes to interactive elements for test targeting.

Wire-format facts that make assertions land first time:

  • The test harness renders through createElement, a different code path from the device's streaming collector. A prop can work in tests and be missing on device, or vice versa.
  • Fills live at style.bg_color as #AARRGGBB — there is no props.bg.
  • uppercase is applied by the renderer, so the text prop keeps its original case.
  • layout.position is [top, right, bottom, left]; layout.position_type === 1 means absolute.
  • assertElement($type, $matcher) + tree() for structural assertions; guard position with an is_array/count check, since not every node carries one.

Phase 5 — Integrate + cutover

  1. Integrator: fix reported blockers, verify every route resolves (NativeRouter->resolve() returns ['class','layout','params']), run the full native suite plus untouched data-layer suites, and grep native views for hex literals / non-whitelist classes / leftover wire:/flux: syntax.
  2. Only once green: delete app/Livewire, resources/views/livewire, resources/views/flux, legacy resources/views/components, and old Livewire test dirs. Rewrite generic smoke tests (assertSeeLivewire → route-resolution or Native::visit()->assertScreen()).
  3. composer dump-autoload, full suite, vendor/bin/pint --dirty, php artisan native:validate.
  4. Rebuild with native:run on a simulator or emulator as the final check — plugin registration, plugin Swift/Kotlin sources AND bundled fonts all compile in at build time.

Patching the framework during an upgrade

Local path checkouts of mobile / mobile-ui are common on this path. If you add a style utility, it must travel three layers, and the collector has four call sites — missing one produces the classic "works in tests, missing on device" split:

  • mobile/src/Edge/TailwindParser.php — class → attrs. A feature spanning several classes (like gradients) needs a deep-merge branch in parse(), as dark has; a flat array_merge lets the last class clobber the earlier ones.
  • mobile/src/Edge/NativeElementCollector.php — attrs → wire. Sites: openStreaming, leafStreaming (each with a builtin and a plugin branch), and createElement, which is what the test harness uses.
  • NodeStyleModifier.swift / NodeModifiers.kt — the single choke point where every node's background is painted; wiring there makes a utility work on any element.

The node is a packed 160-byte binary struct with fixed offsets, so the style block cannot take variable-length data — ride the props bag instead (dark_bg_color does). Unknown prop keys are fine: the encoder falls back to an inline string for anything outside the interned PropKey table.

Orchestration notes (multi-agent)

  • The conventions.md contract doc is what makes parallel conversion safe — write it before fan-out, append the partials contract to it.
  • Effort scaling: complex screens (multi-step forms, camera, geolocation) get high effort; simple lists don't.
  • Workflow args may arrive as a JSON string — guard with typeof args === 'string' ? JSON.parse(args) : args.
  • Cache-resume: fix a workflow script bug and resume with the same run ID; completed agents replay free.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib