Skip to content
Draft
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
10 changes: 10 additions & 0 deletions app/Filament/Resources/PluginResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ public static function form(Schema $schema): Schema

Forms\Components\Toggle::make('is_active')
->label('Active'),

Forms\Components\Toggle::make('works_in_jump')
->label('Works in Jump')
->helperText('Show a badge indicating this plugin runs in the Jump preview app (i.e. it ships no custom native code)'),
]),

Schemas\Components\Section::make('Review Checks')
Expand Down Expand Up @@ -312,6 +316,10 @@ public static function table(Table $table): Table
->label('Active')
->sortable(),

Tables\Columns\ToggleColumn::make('works_in_jump')
->label('Jump')
->sortable(),

Tables\Columns\IconColumn::make('reviewed_at')
->label('Reviewed')
->boolean()
Expand Down Expand Up @@ -346,6 +354,8 @@ public static function table(Table $table): Table
Tables\Filters\TernaryFilter::make('featured'),
Tables\Filters\TernaryFilter::make('is_active')
->label('Active'),
Tables\Filters\TernaryFilter::make('works_in_jump')
->label('Works in Jump'),
])
->actions([
Actions\ActionGroup::make([
Expand Down
23 changes: 6 additions & 17 deletions app/Http/Controllers/ApplinksController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,10 @@

namespace App\Http\Controllers;

use App\Support\JumpApp;

class ApplinksController extends Controller
{
/**
* Jump's iOS app identity: <TeamID>.<BundleID>.
*/
private const IOS_APP_ID = 'J68WFCX458.com.bifrosttech.jump';

/**
* Jump's Android package + the SHA-256 of its Play app-signing cert.
* These are public identifiers, not secrets.
*/
private const ANDROID_PACKAGE = 'com.bifrosttech.jump';

private const ANDROID_SHA256 = 'D8:31:4E:55:E5:FF:06:17:D8:49:EA:3B:1F:BF:6C:58:B3:8D:AD:2C:30:CA:13:D2:CA:42:B0:85:B4:7D:CB:38';

/**
* URL path prefixes that open in Jump.
*
Expand Down Expand Up @@ -46,9 +35,9 @@ public function assetLinks()
],
'target' => [
'namespace' => 'android_app',
'package_name' => self::ANDROID_PACKAGE,
'package_name' => JumpApp::ANDROID_PACKAGE,
'sha256_cert_fingerprints' => [
self::ANDROID_SHA256,
JumpApp::ANDROID_SHA256,
],
],
],
Expand Down Expand Up @@ -76,13 +65,13 @@ public function appSiteAssociation()
'applinks' => [
'details' => [
[
'appIDs' => [self::IOS_APP_ID],
'appIDs' => [JumpApp::IOS_APP_ID],
'components' => $components,
],
],
],
'webcredentials' => [
'apps' => [self::IOS_APP_ID],
'apps' => [JumpApp::IOS_APP_ID],
],
]);
}
Expand Down
22 changes: 16 additions & 6 deletions app/Http/Controllers/ShowDocumentationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Support\CommonMark\CommonMark;
use Artesaos\SEOTools\Facades\SEOMeta;
use Artesaos\SEOTools\Facades\SEOTools;
use Closure;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
Expand All @@ -24,16 +25,12 @@ class ShowDocumentationController extends Controller
{
public function __invoke(Request $request, string $platform, string $version, ?string $page = null)
{
if (config('app.env') === 'local') {
Cache::flush();
}

abort_unless(is_dir(resource_path('views/docs/'.$platform.'/'.$version)), 404);

session(['viewing_docs_version' => $version]);
session(['viewing_docs_platform' => $platform]);

$navigation = Cache::remember("docs_nav_{$platform}_{$version}", now()->addDay(),
$navigation = $this->cacheOrCompute("docs_nav_{$platform}_{$version}",
fn () => $this->getNavigation($platform, $version)
);

Expand All @@ -42,7 +39,7 @@ public function __invoke(Request $request, string $platform, string $version, ?s
}

try {
$pageProperties = Cache::remember("docs_{$platform}_{$version}_{$page}", now()->addDay(),
$pageProperties = $this->cacheOrCompute("docs_{$platform}_{$version}_{$page}",
fn () => $this->getPageProperties($platform, $version, $page)
);
} catch (InvalidArgumentException $e) {
Expand Down Expand Up @@ -83,6 +80,19 @@ public function __invoke(Request $request, string $platform, string $version, ?s
return view('docs.index')->with($pageProperties);
}

/**
* Cache the callback's result for a day, or compute it fresh in local so
* docs edits show up immediately without clearing (or racing on) the cache.
*/
private function cacheOrCompute(string $key, Closure $callback): mixed
{
if (config('app.env') === 'local') {
return $callback();
}

return Cache::remember($key, now()->addDay(), $callback);
}

public function serveRawMarkdown(Request $request, string $platform, string $version, string $page)
{
abort_unless(is_dir(resource_path('views/docs/'.$platform.'/'.$version)), 404);
Expand Down
6 changes: 6 additions & 0 deletions app/Models/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ public function isOfficial(): bool
return $this->is_official ?? false;
}

public function worksInJump(): bool
{
return $this->works_in_jump ?? false;
}

public function isSatisSynced(): bool
{
return $this->satis_synced_at !== null;
Expand Down Expand Up @@ -729,6 +734,7 @@ protected function casts(): array
'featured' => 'boolean',
'is_active' => 'boolean',
'is_official' => 'boolean',
'works_in_jump' => 'boolean',
'composer_data' => 'array',
'nativephp_data' => 'array',
'last_synced_at' => 'datetime',
Expand Down
54 changes: 54 additions & 0 deletions app/Support/JumpApp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Support;

use App\Http\Controllers\ApplinksController;

/**
* Public identity of Bifrost's Jump preview app.
*
* Single source of truth for the app's store URLs and platform identifiers,
* shared by the deep-link association files ({@see ApplinksController})
* and the docs "open in Jump" UI. These are public identifiers, not secrets.
*/
final class JumpApp
{
/**
* Jump's iOS app identity: <TeamID>.<BundleID>.
*/
public const IOS_APP_ID = 'J68WFCX458.com.bifrosttech.jump';

public const IOS_APP_STORE_URL = 'https://apps.apple.com/us/app/bifrost-jump/id6757173334';

public const ANDROID_PACKAGE = 'com.bifrosttech.jump';

/**
* SHA-256 of Jump's Play app-signing cert (public, not a secret).
*/
public const ANDROID_SHA256 = 'D8:31:4E:55:E5:FF:06:17:D8:49:EA:3B:1F:BF:6C:58:B3:8D:AD:2C:30:CA:13:D2:CA:42:B0:85:B4:7D:CB:38';

public const ANDROID_PLAY_STORE_URL = 'https://play.google.com/store/apps/details?id=com.bifrosttech.jump';

/**
* The only domain whose deep links open in Jump.
*/
public const CANONICAL_DOMAIN = 'https://nativephp.com';

/**
* Marks a visit as originating from a scanned QR code.
*/
public const QR_PARAM = 'jump=qr';

/**
* Build the scannable deep link for a docs page.
*
* Deliberately pinned to the canonical domain rather than the current host:
* Jump only claims links for nativephp.com (see {@see ApplinksController}),
* and a phone can't resolve a local or preview hostname anyway — so a QR
* built from url() would be unscannable everywhere except production.
*/
public static function docsDeepLink(string $path): string
{
return self::CANONICAL_DOMAIN.'/'.ltrim($path, '/').'?'.self::QR_PARAM;
}
}
41 changes: 41 additions & 0 deletions app/Support/QrCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace App\Support;

use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Output\QROutputInterface;
use chillerlan\QRCode\QRCode as QRCodeGenerator;
use chillerlan\QRCode\QROptions;

/**
* Thin wrapper around chillerlan/php-qrcode (available via filament/filament)
* that renders scannable, inline SVG QR codes.
*/
class QrCode
{
/**
* Render the given data as inline SVG markup.
*
* Dark modules are filled with #000, so the SVG must be placed on a light
* background (a white container) to stay scannable in both colour schemes.
*/
public static function svg(string $data): string
{
$options = new QROptions([
'outputType' => QROutputInterface::MARKUP_SVG,
'outputBase64' => false,
'eccLevel' => EccLevel::M,
'addQuietzone' => true,
'quietzoneSize' => 2,
'drawLightModules' => false,
'svgAddXmlHeader' => false,
'cssClass' => 'jump-qr',
]);

$svg = (new QRCodeGenerator($options))->render($data);

// chillerlan emits a viewBox-only <svg> (no width/height) so it can scale;
// pin it to fill its container as a crisp square.
return str_replace('<svg ', '<svg style="display:block;width:100%;height:auto" ', $svg);
}
}
7 changes: 7 additions & 0 deletions database/factories/PluginFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ public function inactive(): static
]);
}

public function worksInJump(): static
{
return $this->state(fn (array $attributes) => [
'works_in_jump' => true,
]);
}

public function free(): static
{
return $this->state(fn (array $attributes) => [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('plugins', function (Blueprint $table) {
$table->boolean('works_in_jump')->default(false)->after('mobile_min_version');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('plugins', function (Blueprint $table) {
$table->dropColumn('works_in_jump');
});
}
};
101 changes: 101 additions & 0 deletions resources/views/components/docs/jump-app-overlay.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{{--
Shown when a QR visitor lands here without the Jump app installed.

Must stay outside the desktop-only right sidebar: it is the phone — not the
desktop — that renders this, after the deep link falls back to the browser.
--}}
<div
x-cloak
x-data="{
show: new URLSearchParams(window.location.search).get('jump') === 'qr',
platform: (() => {
const ua = navigator.userAgent || '';
if (/iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) return 'ios';
if (/android/i.test(ua)) return 'android';
return 'unknown';
})(),
dismiss() { this.show = false; },
}"
x-show="show"
x-effect="document.documentElement.style.overflow = show ? 'hidden' : ''"
x-on:keydown.escape.window="dismiss()"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-labelledby="jump-overlay-title"
>
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" x-on:click="dismiss()" aria-hidden="true"></div>

<div
x-show="show"
x-transition
class="relative w-full max-w-sm rounded-2xl border border-gray-200 bg-white p-6 text-center shadow-xl dark:border-gray-700 dark:bg-slate-900"
>
<button
type="button"
x-on:click="dismiss()"
class="absolute right-4 top-4 text-gray-400 transition hover:text-gray-600 dark:hover:text-gray-200"
aria-label="Dismiss"
>
<x-heroicon-o-x-mark class="size-5" />
</button>

<div class="mx-auto grid size-12 place-items-center rounded-xl bg-indigo-100 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-400">
<x-heroicon-o-bolt class="size-6" aria-hidden="true" />
</div>

<h2 id="jump-overlay-title" class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
Get the Jump app
</h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
You'll need the free Jump app to preview this page on your device. Install it, then scan the code again.
</p>

<div class="mt-6 flex flex-col gap-3">
{{-- iOS: shown on Apple devices, or when the platform can't be detected. --}}
<a
x-show="platform === 'ios' || platform === 'unknown'"
href="{{ \App\Support\JumpApp::IOS_APP_STORE_URL }}"
target="_blank"
rel="noopener"
class="inline-flex items-center justify-center gap-3 rounded-xl bg-black px-5 py-2.5 text-white transition hover:bg-gray-800"
>
<svg viewBox="0 0 24 24" fill="currentColor" class="size-6 shrink-0" aria-hidden="true">
<path d="M17.05 12.04c-.03-2.6 2.12-3.85 2.22-3.91-1.21-1.77-3.09-2.01-3.76-2.04-1.6-.16-3.12.94-3.93.94-.81 0-2.06-.92-3.39-.9-1.74.03-3.35 1.01-4.25 2.57-1.81 3.14-.46 7.79 1.3 10.34.86 1.25 1.88 2.65 3.22 2.6 1.29-.05 1.78-.83 3.34-.83 1.56 0 2 .83 3.37.81 1.39-.03 2.27-1.27 3.12-2.53.98-1.45 1.39-2.85 1.41-2.92-.03-.01-2.71-1.04-2.74-4.13M14.53 4.5c.72-.87 1.2-2.08 1.07-3.28-1.03.04-2.28.69-3.02 1.56-.66.77-1.24 2-1.08 3.18 1.15.09 2.32-.59 3.03-1.46"/>
</svg>
<span class="text-left leading-tight">
<span class="block text-[10px] uppercase tracking-wide opacity-80">Download on the</span>
<span class="-mt-0.5 block text-base font-semibold">App Store</span>
</span>
</a>

{{-- Android: shown on Android devices, or when the platform can't be detected. --}}
<a
x-show="platform === 'android' || platform === 'unknown'"
href="{{ \App\Support\JumpApp::ANDROID_PLAY_STORE_URL }}"
target="_blank"
rel="noopener"
class="inline-flex items-center justify-center gap-3 rounded-xl bg-black px-5 py-2.5 text-white transition hover:bg-gray-800"
>
<svg viewBox="0 0 24 24" class="size-6 shrink-0" aria-hidden="true">
<path fill="#00d4ff" d="M3.6 2.3c-.3.3-.4.7-.4 1.2v17c0 .5.1.9.4 1.2l.1.1L13 12.6v-.2z"/>
<path fill="#00f076" d="M16.1 15.7l-3.1-3.1v-.2l3.1-3.1.1.1 3.7 2.1c1 .6 1 1.6 0 2.2z"/>
<path fill="#ff3a44" d="M16.2 15.8L13 12.6 3.6 22c.4.3.9.4 1.5 0z"/>
<path fill="#ffc400" d="M3.6 2.3L13 11.7l3.2-3.2-11-6.2c-.6-.3-1.2-.3-1.6 0z"/>
</svg>
<span class="text-left leading-tight">
<span class="block text-[10px] uppercase tracking-wide opacity-80">Get it on</span>
<span class="-mt-0.5 block text-base font-semibold">Google Play</span>
</span>
</a>
</div>

<button
type="button"
x-on:click="dismiss()"
class="mt-4 text-xs font-medium text-gray-400 transition hover:text-gray-600 dark:hover:text-gray-300"
>
Already installed? Scan the code again.
</button>
</div>
</div>
Loading
Loading