mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-20 05:30:30 +00:00
Add headless Nostr launcher page for the mobile app
A direct ACTION_VIEW intent to nostrsigner: (Browser::open from the app) lacks category.BROWSABLE, so Amber routes it into its app-to-app path and rejects it as malformed. The app instead opens /auth/mobile/nostr in an in-app browser; that page fires the signer via window.location, so the intent carries BROWSABLE and Amber uses its web-signing flow. No visible login UI, local signing, token returned via the App Link.
This commit is contained in:
@@ -125,6 +125,53 @@ final class MobileAuthController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headless Nostr launcher for the mobile app.
|
||||||
|
*
|
||||||
|
* The app opens this page in an in-app browser (Chrome Custom Tab). It
|
||||||
|
* immediately launches the NIP-55 signer (e.g. Amber) via window.location
|
||||||
|
* so the intent carries category.BROWSABLE — which routes Amber into its
|
||||||
|
* web-signing flow (a direct ACTION_VIEW intent without that category
|
||||||
|
* lands in Amber's app-to-app path and is rejected as malformed).
|
||||||
|
*
|
||||||
|
* The signer signs the kind-22242 challenge locally and opens the
|
||||||
|
* /auth/mobile/signed callback, which issues the token and hands it back
|
||||||
|
* to the app via the verified App Link. No relay, no visible login UI.
|
||||||
|
*/
|
||||||
|
public function nostrLauncher(Request $request)
|
||||||
|
{
|
||||||
|
$deviceName = str((string) $request->query('device_name', self::DEFAULT_DEVICE_NAME))
|
||||||
|
->limit(64, '')
|
||||||
|
->whenEmpty(fn () => str(self::DEFAULT_DEVICE_NAME))
|
||||||
|
->value();
|
||||||
|
|
||||||
|
// The signed callback issues the token and reads the device name
|
||||||
|
// from this session (the callback shares the Custom Tab's cookies).
|
||||||
|
$request->session()->put('mobile_auth', [
|
||||||
|
'redirect_uri' => self::ALLOWED_REDIRECT_URIS[0],
|
||||||
|
'device_name' => $deviceName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$k1 = bin2hex(random_bytes(32));
|
||||||
|
|
||||||
|
$event = [
|
||||||
|
'kind' => 22242,
|
||||||
|
'created_at' => now()->timestamp,
|
||||||
|
'content' => '',
|
||||||
|
'tags' => [['challenge', $k1]],
|
||||||
|
];
|
||||||
|
|
||||||
|
$signerUri = 'nostrsigner:'.rawurlencode(json_encode($event)).'?'.http_build_query([
|
||||||
|
'compressionType' => 'none',
|
||||||
|
'returnType' => 'event',
|
||||||
|
'type' => 'sign_event',
|
||||||
|
'appName' => 'Einundzwanzig',
|
||||||
|
'callbackUrl' => url('/auth/mobile/signed/'.$k1.'/'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('auth.mobile-nostr-launch', ['signerUri' => $signerUri]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browser fallback for the app handoff URL (/app/auth?token=…).
|
* Browser fallback for the app handoff URL (/app/auth?token=…).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ __('Anmeldung mit Nostr') }} — Einundzwanzig</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; min-height: 100dvh; display: flex; align-items: center; justify-content: center;
|
||||||
|
background: #09090b; color: #fafafa; font-family: ui-sans-serif, system-ui, sans-serif; }
|
||||||
|
.card { text-align: center; padding: 2rem; max-width: 22rem; }
|
||||||
|
h1 { font-size: 1.25rem; margin: 1rem 0 .5rem; }
|
||||||
|
p { color: #a1a1aa; line-height: 1.5; }
|
||||||
|
a.button { display: inline-block; margin-top: 1.5rem; padding: .875rem 1.25rem; border-radius: .75rem;
|
||||||
|
background: #f7931a; color: #09090b; font-weight: 600; text-decoration: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>{{ __('Anmeldung mit Nostr') }}</h1>
|
||||||
|
<p>{{ __('Dein Nostr-Signer (z. B. Amber) öffnet sich gleich. Falls nicht, tippe auf den Button.') }}</p>
|
||||||
|
<a class="button" href="{{ $signerUri }}">{{ __('Signer öffnen') }}</a>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Launch via window.location so the intent carries category.BROWSABLE
|
||||||
|
// and Amber routes it into its web-signing flow.
|
||||||
|
window.location.href = @js($signerUri);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -44,6 +44,12 @@ Route::livewire('/auth/mobile', 'auth.mobile-login')
|
|||||||
->middleware('throttle:30,1')
|
->middleware('throttle:30,1')
|
||||||
->name('auth.mobile');
|
->name('auth.mobile');
|
||||||
|
|
||||||
|
// Headless Nostr launcher: opened by the app in an in-app browser, fires
|
||||||
|
// the NIP-55 signer (Amber) with category.BROWSABLE via window.location.
|
||||||
|
Route::get('/auth/mobile/nostr', [MobileAuthController::class, 'nostrLauncher'])
|
||||||
|
->middleware('throttle:30,1')
|
||||||
|
->name('auth.mobile.nostr');
|
||||||
|
|
||||||
Route::get('/auth/mobile/complete/{k1}', [MobileAuthController::class, 'complete'])
|
Route::get('/auth/mobile/complete/{k1}', [MobileAuthController::class, 'complete'])
|
||||||
->where('k1', '[a-f0-9]{64}')
|
->where('k1', '[a-f0-9]{64}')
|
||||||
->middleware('throttle:30,1')
|
->middleware('throttle:30,1')
|
||||||
|
|||||||
Reference in New Issue
Block a user