diff --git a/app/Devnull/IssueCommand.php b/app/Devnull/IssueCommand.php index 57a1cb2..58376dc 100644 --- a/app/Devnull/IssueCommand.php +++ b/app/Devnull/IssueCommand.php @@ -8,7 +8,7 @@ /** * The web's `/feature` and `/bug` commands, which turn a new commit's draft - * into a public issue on nunomaduro/dev-null.sh, opened by devnull's GitHub + * into a public issue on dev-null-sh/dev-null.sh, opened by devnull's GitHub * App and crediting you. The wording, and when each is suggested, are the * web's (resources/js/lib/issue-commands.ts). */ @@ -20,7 +20,7 @@ enum IssueCommand: string /** * Where the issue goes, as the web says it under the command. */ - public const string Repository = 'nunomaduro/dev-null.sh'; + public const string Repository = 'dev-null-sh/dev-null.sh'; /** * Issues still need three literal characters and do not discount URLs. diff --git a/app/Devnull/IssueEnvironment.php b/app/Devnull/IssueEnvironment.php new file mode 100644 index 0000000..71b957b --- /dev/null +++ b/app/Devnull/IssueEnvironment.php @@ -0,0 +1,42 @@ + + */ + public static function collect(): array + { + $info = json_decode((string) Device::getInfo(), true); + $info = is_array($info) ? $info : []; + $text = fn (string $key): string => is_string($info[$key] ?? null) ? $info[$key] : ''; + + $environment = [ + 'browser' => 'devnull app '.config('nativephp.version'), + 'os' => $text('operatingSystem').' '.$text('osVersion'), + 'language' => $text('language'), + 'color_scheme' => System::appearance(), + ]; + + $environment = array_map(fn (string $value): string => mb_substr(trim($value), 0, self::MaxLength), $environment); + + return array_filter($environment, fn (string $value): bool => $value !== ''); + } +} diff --git a/app/NativeComponents/Concerns/DraftsIssues.php b/app/NativeComponents/Concerns/DraftsIssues.php index 3580918..ffa1d0e 100644 --- a/app/NativeComponents/Concerns/DraftsIssues.php +++ b/app/NativeComponents/Concerns/DraftsIssues.php @@ -7,6 +7,7 @@ use App\Devnull\DraftImages; use App\Devnull\Exceptions\ApiException; use App\Devnull\IssueCommand; +use App\Devnull\IssueEnvironment; use App\Devnull\SiteLink; use App\Devnull\Stash; use App\Devnull\Toasts; @@ -37,14 +38,16 @@ * it, which offers the command with the draft kept. Its X stops the hints * on this phone, as the web's stops them in that browser. * - * Opening the issue waits on GitHub, so it's sent in the background while the - * button spins (see WritesInBackground). Once it's open the draft goes, with - * any photos it had, and the sheet says "Opened issue #123." with View issue, - * as the web's toast does, or a toast does, with View issue as its button, if - * the sheet was swiped away by then (see SyncsOutbox::toastActionPressed()). - * When it isn't, the draft stays an issue, with why under it: devnull's own - * words when it sends them, else the web's, which say to check upstream when - * the issue might have been opened anyway. + * Opening the issue waits on GitHub, so it's sent in the background while + * the button spins (see WritesInBackground), with the app and device + * details the web sends about its browser (see IssueEnvironment). Once it's + * open the draft goes, with any photos it had, and the sheet says "Opened + * issue #123." with View issue, as the web's toast does, or a toast does, + * with View issue as its button, if the sheet was swiped away by then (see + * SyncsOutbox::toastActionPressed()). When it isn't, the draft stays an + * issue, with why under it: devnull's own words when it sends them, else + * the web's, which say to check upstream when the issue might have been + * opened anyway. */ trait DraftsIssues { @@ -134,7 +137,7 @@ public function openIssueFromSheet(): void $this->openingIssue = true; $this->sheetError = null; - $this->writeInBackground('POST', Client::IssuesPath, ['command' => $command->command(), 'body' => $body], function (array $json): void { + $this->writeInBackground('POST', Client::IssuesPath, ['command' => $command->command(), 'body' => $body, 'environment' => IssueEnvironment::collect()], function (array $json): void { $number = is_int($json['number'] ?? null) ? $json['number'] : null; $url = is_string($json['url'] ?? null) ? $json['url'] : null; $stash = app(Stash::class); diff --git a/resources/views/native/partials/issue-badge.blade.php b/resources/views/native/partials/issue-badge.blade.php index 410db8c..b5cb86b 100644 --- a/resources/views/native/partials/issue-badge.blade.php +++ b/resources/views/native/partials/issue-badge.blade.php @@ -1,6 +1,7 @@ {{-- The web's IssueCommandBadge, over the field of a draft that's an issue: $command in a soft chip, whose X turns the - draft back into a commit, then where the issue goes. The X's target is the chip's full height. + draft back into a commit, then where the issue goes and what goes with it (see IssueEnvironment). That line wraps + rather than truncating, so the device details it mentions are never cut off. The X's target is the chip's full height. --}} @@ -12,5 +13,5 @@ - Public issue on {{ \App\Devnull\IssueCommand::Repository }}, crediting you + Public issue on {{ \App\Devnull\IssueCommand::Repository }}, crediting you, with your app and device details diff --git a/tests/Feature/Screens/IssueCommandsTest.php b/tests/Feature/Screens/IssueCommandsTest.php index c0299aa..d09611d 100644 --- a/tests/Feature/Screens/IssueCommandsTest.php +++ b/tests/Feature/Screens/IssueCommandsTest.php @@ -3,12 +3,14 @@ use App\Devnull\Client; use App\Devnull\Data\StashedDraft; use App\Devnull\IssueCommand; +use App\Devnull\IssueEnvironment; use App\Devnull\Stash; use App\NativeComponents\Log; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; +use Native\Mobile\Facades\System; use Native\Mobile\Testing\Native; use Native\Mobile\Testing\TestableComponent; use Victorycodedev\ToastKit\Events\ToastActionPressed; @@ -29,7 +31,7 @@ function issueSheetOnLog(?array $answer = null): TestableComponent { serveApi([ 'GET /api/v1/log/following' => [pageJson([commitJson(['id' => 'c1', 'body' => 'An older commit', 'author' => userJson(['id' => 'u2', 'handle' => 'ada'])])])], - 'POST /api/v1/issues' => $answer ?? [['number' => 123, 'url' => 'https://github.com/nunomaduro/dev-null.sh/issues/123'], 201], + 'POST /api/v1/issues' => $answer ?? [['number' => 123, 'url' => 'https://github.com/dev-null-sh/dev-null.sh/issues/123'], 201], ]); return Native::visit('/log', platform: 'ios')->press('composeInSheet'); @@ -53,7 +55,10 @@ function hasRef(TestableComponent $screen, string $ref): bool ->and(hasRef($screen, 'sheet-open-issue'))->toBeTrue() ->and(hasRef($screen, 'sheet-commit'))->toBeFalse(); - $screen->assertSee($command)->assertSee('Public issue on nunomaduro/dev-null.sh, crediting you'); + $screen->assertSee($command)->assertSee('Public issue on dev-null-sh/dev-null.sh, crediting you, with your app and device details'); + + // It says what goes with the issue, so it wraps rather than being cut off. + expect(findNode(nodeByRef($screen, 'issue-badge'), fn (array $node): bool => str_starts_with($node['props']['text'] ?? '', 'Public issue on'))['props'])->not->toHaveKey('max_lines'); })->with([ 'feature' => ['/feature Dark mode', '/feature', 'Dark mode', IssueCommand::Feature->placeholder()], 'bug' => ['/bug ', '/bug', '', IssueCommand::Bug->placeholder()], @@ -184,7 +189,8 @@ function hasRef(TestableComponent $screen, string $ref): bool Http::assertSent(fn (Request $request): bool => $request->method() === 'POST' && $request->url() === 'http://devnull.test/api/v1/'.Client::IssuesPath - && $request->body() === '{"command":"/bug","body":"Search loses its filters\n\nSteps: select PHP."}' + && $request['command'] === '/bug' + && $request['body'] === "Search loses its filters\n\nSteps: select PHP." && $request->hasHeader('X-SSH-Signature')); expect(issueRequestsSent())->toBe(1) @@ -195,7 +201,7 @@ function hasRef(TestableComponent $screen, string $ref): bool $bridge->assertNotCalled('ToastKit.Show'); $screen->tap('view-issue') - ->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://github.com/nunomaduro/dev-null.sh/issues/123'); + ->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://github.com/dev-null-sh/dev-null.sh/issues/123'); $screen->tap('issue-done'); expect(nodeByRef($screen, 'sheet')['props']['visible'])->toBeFalse(); @@ -206,6 +212,37 @@ function hasRef(TestableComponent $screen, string $ref): bool ->and(hasRef($screen, 'sheet-commit'))->toBeTrue(); }); +it('sends the app and device details the web sends about its browser, never the phone\'s name', function () { + config(['nativephp.version' => '1.4.0']); + Native::fakeBridge()->respondTo('Device.GetInfo', ['info' => json_encode([ + 'name' => "Ada's iPhone", + 'model' => 'iPhone', + 'operatingSystem' => 'iOS', + 'osVersion' => '27.2', + 'language' => 'en-GB', + ])]); + System::rememberAppearance('dark'); + + try { + issueSheetOnLog()->input('sheet-body', '/bug Broken.')->tap('sheet-open-issue'); + } finally { + System::rememberAppearance('light'); + } + + Http::assertSent(fn (Request $request): bool => str_ends_with($request->url(), '/api/v1/'.Client::IssuesPath) + && $request['environment'] === ['browser' => 'devnull app 1.4.0', 'os' => 'iOS 27.2', 'language' => 'en-GB', 'color_scheme' => 'dark'] + && ! str_contains($request->body(), 'Ada')); +}); + +it('leaves out the details the phone does not give, and cuts the rest to what devnull takes', function () { + config(['nativephp.version' => str_repeat('9', 120)]); + + issueSheetOnLog()->input('sheet-body', '/bug Broken.')->tap('sheet-open-issue'); + + Http::assertSent(fn (Request $request): bool => str_ends_with($request->url(), '/api/v1/'.Client::IssuesPath) + && $request['environment'] === ['browser' => mb_substr('devnull app '.str_repeat('9', 120), 0, IssueEnvironment::MaxLength), 'color_scheme' => 'light']); +}); + it('opens no issue link that is not a web page', function () { $screen = issueSheetOnLog([['number' => 123, 'url' => 'javascript:alert(1)'], 201])->input('sheet-body', '/bug Broken.')->tap('sheet-open-issue'); @@ -222,13 +259,13 @@ function hasRef(TestableComponent $screen, string $ref): bool ->assertNativeCalled('ToastKit.Show', fn (array $params): bool => $params['message'] === 'Opened issue #123.' && $params['variant'] === 'success' && $params['action'] === ['id' => 'view-issue', 'label' => 'View issue'] - && $params['data'] === ['url' => 'https://github.com/nunomaduro/dev-null.sh/issues/123']); + && $params['data'] === ['url' => 'https://github.com/dev-null-sh/dev-null.sh/issues/123']); expect(app(Stash::class)->get($this->userId, 'commit')->body)->toBe('') ->and(nodeByRef($screen, 'sheet')['props']['visible'])->toBeFalse(); - $screen->emitNative(ToastActionPressed::class, ['toastId' => 'toast-1', 'actionId' => 'view-issue', 'data' => ['url' => 'https://github.com/nunomaduro/dev-null.sh/issues/123']]) - ->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://github.com/nunomaduro/dev-null.sh/issues/123'); + $screen->emitNative(ToastActionPressed::class, ['toastId' => 'toast-1', 'actionId' => 'view-issue', 'data' => ['url' => 'https://github.com/dev-null-sh/dev-null.sh/issues/123']]) + ->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://github.com/dev-null-sh/dev-null.sh/issues/123'); }); it('keeps the issue draft with why when devnull or GitHub says no', function (array $answer, string $message) {