Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
162 changes: 155 additions & 7 deletions src/Edge/NativeRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

namespace Native\Mobile\Edge;

use Illuminate\Http\Request;
use Illuminate\Routing\Pipeline;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Facade;
use Symfony\Component\HttpFoundation\Response;

class NativeRouter
{
/**
Expand Down Expand Up @@ -80,9 +86,9 @@ protected static function renderPlaceholder(): void
* URI → component class registry.
* Populated by Route::native() calls.
*
* Each entry: ['class' => string, 'layout' => ?string]
* Each entry: ['class' => string, 'layout' => ?string, 'route' => ?Route]
*
* @var array<string, array{class: string, layout: ?string}>
* @var array<string, array{class: string, layout: ?string, route: ?Route}>
*/
protected static array $routes = [];

Expand Down Expand Up @@ -128,12 +134,13 @@ public function flushDeferredTransition(): void

// ── Static registry ─────────────────────────────

public static function register(string $uri, string $class, ?string $layout = null): void
public static function register(string $uri, string $class, ?string $layout = null, ?Route $route = null): void
{
$pattern = '/'.ltrim($uri, '/');
static::$routes[$pattern] = [
'class' => $class,
'layout' => $layout ?? static::$currentGroupLayout,
'route' => $route,
];
}

Expand All @@ -145,7 +152,10 @@ public static function register(string $uri, string $class, ?string $layout = nu
*/
public static function registeredRoutes(): array
{
return static::$routes;
return array_map(fn (array $entry) => [
'class' => $entry['class'],
'layout' => $entry['layout'],
], static::$routes);
}

/**
Expand Down Expand Up @@ -338,6 +348,12 @@ public function preloadStack(array $entries): void
continue;
}

if ($this->runRouteMiddleware($uri) !== null) {
static::debugLog("preloadStack: skipped $uri — route middleware blocked navigation");

continue;
}

try {
$component = $this->createComponent(
$resolved['class'],
Expand Down Expand Up @@ -367,9 +383,9 @@ public function preloadStack(array $entries): void
* Entry point. Init shared memory, run the navigation loop,
* shutdown when done.
*
* @return string|null Exit URI for redirect, or null
* @return Response|string|null Middleware response, exit URI, or null
*/
public function start(string $class, array $params = [], string $uri = ''): ?string
public function start(string $class, array $params = [], string $uri = ''): Response|string|null
{
NativeComponent::registerDumpHandler();

Expand Down Expand Up @@ -406,7 +422,7 @@ public function start(string $class, array $params = [], string $uri = ''): ?str
* Navigation loop — runs until the stack is empty or
* we need to exit to a web route.
*/
protected function loop(): ?string
protected function loop(): Response|string|null
{
$freshPush = true;

Expand Down Expand Up @@ -474,6 +490,15 @@ protected function loop(): ?string
return $intent->uri;
}

$middlewareResponse = $this->runRouteMiddleware($intent->uri);
if ($middlewareResponse !== null) {
static::debugLog('NAVIGATE: route middleware blocked navigation with status '.$middlewareResponse->getStatusCode());
$component->unmount();
$this->stack = [];

return $middlewareResponse;
}

static::debugLog("NAVIGATE: resolved to {$resolved['class']}, deferring transition");
$this->deferredTransition = $intent->transition ?? Transition::SlideFromRight;

Expand Down Expand Up @@ -525,6 +550,15 @@ protected function loop(): ?string
return $intent->uri;
}

$middlewareResponse = $this->runRouteMiddleware($intent->uri);
if ($middlewareResponse !== null) {
static::debugLog('REPLACE: route middleware blocked navigation with status '.$middlewareResponse->getStatusCode());
$component->unmount();
$this->stack = [];

return $middlewareResponse;
}

static::debugLog("REPLACE: resolved to {$resolved['class']}");
$component->unmount();
array_pop($this->stack);
Expand Down Expand Up @@ -584,6 +618,120 @@ protected function loop(): ?string
return null;
}

/**
* Run the Laravel route middleware associated with an in-app navigation.
*
* A null response means the middleware pipeline reached its destination.
* Any response means middleware short-circuited and the screen must not mount.
*/
protected function runRouteMiddleware(string $uri): ?Response
{
$resolved = static::resolve($uri);
if ($resolved === null) {
return null;
}

$pattern = $this->routePatternFor($uri);
$registeredRoute = $pattern !== null ? (static::$routes[$pattern]['route'] ?? null) : null;
if (! $registeredRoute instanceof Route) {
return null;
}
$route = clone $registeredRoute;

$currentRequest = app('request');
$path = parse_url($uri, PHP_URL_PATH) ?: '/';
$queryString = parse_url($uri, PHP_URL_QUERY) ?: '';
parse_str($queryString, $query);

$server = $currentRequest->server->all();
$server['REQUEST_METHOD'] = 'GET';
$server['REQUEST_URI'] = $uri;
$server['PATH_INFO'] = $path;
$server['QUERY_STRING'] = $queryString;

/** @var Request $request */
$request = $currentRequest->duplicate(
query: $query,
request: [],
attributes: [],
files: [],
server: $server,
);
$request->setMethod('GET');

$route->setContainer(app());
$route->bind($request);
$request->setRouteResolver(fn () => $route);

$router = app('router');
$currentRoute = $router->getCurrentRoute();
$currentRouterRequest = $router->getCurrentRequest();
$middleware = app()->bound('middleware.disable') && app('middleware.disable') === true
? []
: $router->gatherRouteMiddleware($route);
$destination = new \stdClass;

$hadRouteBinding = app()->bound(Route::class);
$currentRouteBinding = $hadRouteBinding ? app(Route::class) : null;

app()->instance('request', $request);
app()->instance(Route::class, $route);
Facade::clearResolvedInstance('request');

// Router has no public current-route setter, but route middleware may
// legitimately use the Route facade. Scope the same context Laravel's
// normal dispatch establishes, then restore the long-lived request.
(function (Route $route): void {
$this->current = $route;
})->call($router, $route);
(function (Request $request): void {
$this->currentRequest = $request;
})->call($router, $request);

try {
$result = (new Pipeline(app()))
->send($request)
->through($middleware)
->then(fn () => $destination);

return $result === $destination
? null
: $router->prepareResponse($request, $result);
} finally {
app()->instance('request', $currentRequest);
if ($hadRouteBinding) {
app()->instance(Route::class, $currentRouteBinding);
} else {
app()->forgetInstance(Route::class);
}
(function (?Route $route): void {
$this->current = $route;
})->call($router, $currentRoute);
(function (?Request $request): void {
$this->currentRequest = $request;
})->call($router, $currentRouterRequest);
Facade::clearResolvedInstance('request');
}
}

protected function routePatternFor(string $uri): ?string
{
$path = '/'.ltrim(parse_url($uri, PHP_URL_PATH) ?: '/', '/');

if (isset(static::$routes[$path])) {
return $path;
}

foreach (array_keys(static::$routes) as $pattern) {
$regex = preg_replace('/\{(\w+)\}/', '[^/]+', $pattern);
if (preg_match('#^'.$regex.'$#', $path)) {
return $pattern;
}
}

return null;
}

protected function createComponent(string $class, array $params = [], array $data = []): NativeComponent
{
$component = new $class;
Expand Down
13 changes: 10 additions & 3 deletions src/NativeServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
use Native\Mobile\Support\Ios\PhpUrlGenerator;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
use Symfony\Component\HttpFoundation\Response;

class NativeServiceProvider extends PackageServiceProvider
{
Expand Down Expand Up @@ -302,9 +303,7 @@ public function packageBooted()
});

Route::macro('native', function (string $uri, string $componentClass) {
NativeRouter::register($uri, $componentClass);

return Route::get($uri, function () use ($componentClass) {
$route = Route::get($uri, function () use ($componentClass) {
// HTTP feature tests ($this->get('/')) must never enter the
// runloop: it blocks in wait_event against the REAL bridge —
// with a live Jump session that's ~90s of reconnect spinning
Expand Down Expand Up @@ -366,6 +365,10 @@ public function packageBooted()

$exitUri = $router->start($componentClass, $params, $path);

if ($exitUri instanceof Response) {
return $exitUri;
}

if ($exitUri !== null) {
return redirect($exitUri);
}
Expand All @@ -377,6 +380,10 @@ public function packageBooted()

return '';
});

NativeRouter::register($uri, $componentClass, route: $route);

return $route;
});

// Route::nativeGroup(layout: TabsLayout::class, function () { ... })
Expand Down
Loading
Loading