From 7d21e83536c38d6e386e44523ce283b3733f917e Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Fri, 23 Oct 2026 12:51:44 -0400 Subject: [PATCH 1/2] feat: record terms acceptance from the app, and meet Apple's rules for user-generated content The app asks people to accept the terms and privacy policy before it sends them to sign in. PUT /api/v1/me/terms records the version it showed, POST /api/v1/devices takes the same version so sign-in records it in one step, and GET /api/v1/me says which version is in force, when they accepted and whether it was that one, so a new version asks again. The terms and privacy policy now say what App Review looks for: zero tolerance for objectionable content, in-app reporting, muting and blocking, action on reports within 24 hours, bans that cover accounts signed in with Apple, Apple's standard clauses for apps with their own terms, and what Sign in with Apple and the iOS app collect. Co-Authored-By: Claude Sonnet 5.5 --- app/Actions/UpdateUserTermsAcceptance.php | 21 ++++++++ app/Http/Controllers/Api/DeviceController.php | 7 ++- .../Controllers/Api/UserTermsController.php | 27 ++++++++++ app/Http/Requests/CreateApiDeviceRequest.php | 5 +- .../Requests/UpdateApiUserTermsRequest.php | 30 +++++++++++ app/Http/Resources/CurrentUserResource.php | 5 +- app/Models/User.php | 15 ++++++ config/legal.php | 19 +++++++ ...00_add_terms_acceptance_to_users_table.php | 22 ++++++++ resources/js/lib/legal.ts | 4 +- resources/js/pages/privacy/show.tsx | 24 +++++++-- resources/js/pages/terms/show.tsx | 38 +++++++++++--- routes/api.php | 2 + .../Api/CurrentUserControllerTest.php | 3 ++ .../Controllers/Api/DeviceControllerTest.php | 20 +++++++ .../Api/UserTermsControllerTest.php | 52 +++++++++++++++++++ 16 files changed, 280 insertions(+), 14 deletions(-) create mode 100644 app/Actions/UpdateUserTermsAcceptance.php create mode 100644 app/Http/Controllers/Api/UserTermsController.php create mode 100644 app/Http/Requests/UpdateApiUserTermsRequest.php create mode 100644 config/legal.php create mode 100644 database/migrations/2026_10_23_120000_add_terms_acceptance_to_users_table.php create mode 100644 tests/Feature/Controllers/Api/UserTermsControllerTest.php diff --git a/app/Actions/UpdateUserTermsAcceptance.php b/app/Actions/UpdateUserTermsAcceptance.php new file mode 100644 index 000000000..d28f23782 --- /dev/null +++ b/app/Actions/UpdateUserTermsAcceptance.php @@ -0,0 +1,21 @@ +update([ + 'terms_accepted_at' => now(), + 'terms_version' => config('legal.terms_version'), + ]); + } +} diff --git a/app/Http/Controllers/Api/DeviceController.php b/app/Http/Controllers/Api/DeviceController.php index b3f786582..7df41a813 100644 --- a/app/Http/Controllers/Api/DeviceController.php +++ b/app/Http/Controllers/Api/DeviceController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api; use App\Actions\CreateDevice; +use App\Actions\UpdateUserTermsAcceptance; use App\Exceptions\ApiException; use App\Http\Requests\CreateApiDeviceRequest; use App\Http\Resources\CurrentUserResource; @@ -19,7 +20,7 @@ * Approves the key that signed the request as a device, for the user who approved the app on the web, and answers * as GET /api/v1/me does. A code redeemed by another request in the meantime gets the same answer as a wrong one. */ - public function store(CreateApiDeviceRequest $request, CreateDevice $action, NotificationGroupsQuery $notifications): CurrentUserResource|JsonResponse + public function store(CreateApiDeviceRequest $request, CreateDevice $action, UpdateUserTermsAcceptance $acceptTerms, NotificationGroupsQuery $notifications): CurrentUserResource|JsonResponse { $device = $action->handle($request->authorizationCode()); @@ -27,6 +28,10 @@ public function store(CreateApiDeviceRequest $request, CreateDevice $action, Not return ApiException::invalidGrant()->render(); } + if ($request->has('terms_version')) { + $acceptTerms->handle($device->user); + } + Auth::setUser($device->user); return CurrentUserResource::make($device->user->loadCounts())->additional([ diff --git a/app/Http/Controllers/Api/UserTermsController.php b/app/Http/Controllers/Api/UserTermsController.php new file mode 100644 index 000000000..62cf2d70c --- /dev/null +++ b/app/Http/Controllers/Api/UserTermsController.php @@ -0,0 +1,27 @@ +handle($user); + + return CurrentUserResource::make($user->loadCounts())->additional([ + 'unread_notifications_count' => $notifications->unreadCount($user), + ]); + } +} diff --git a/app/Http/Requests/CreateApiDeviceRequest.php b/app/Http/Requests/CreateApiDeviceRequest.php index e100a5541..c11f02263 100644 --- a/app/Http/Requests/CreateApiDeviceRequest.php +++ b/app/Http/Requests/CreateApiDeviceRequest.php @@ -12,6 +12,7 @@ use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Validation\Rule; use InvalidArgumentException; /** @@ -51,7 +52,8 @@ public function authorize(SignedRequests $signedRequests): bool } /** - * The verifier is the one the app made its PKCE challenge from (RFC 7636). + * The verifier is the one the app made its PKCE challenge from (RFC 7636). An app that showed the terms and privacy + * policy before sending the person to sign in says which version, and it is recorded as accepted. * * @return array> */ @@ -60,6 +62,7 @@ public function rules(): array return [ 'code' => ['required', 'string', 'max:255'], 'code_verifier' => ['required', 'string', 'regex:/^[A-Za-z0-9._~-]{43,128}$/'], + 'terms_version' => ['sometimes', 'string', Rule::in([config('legal.terms_version')])], ]; } diff --git a/app/Http/Requests/UpdateApiUserTermsRequest.php b/app/Http/Requests/UpdateApiUserTermsRequest.php new file mode 100644 index 000000000..d9cdd6dd7 --- /dev/null +++ b/app/Http/Requests/UpdateApiUserTermsRequest.php @@ -0,0 +1,30 @@ +> + */ + public function rules(): array + { + return [ + 'version' => ['required', 'string', Rule::in([config('legal.terms_version')])], + ]; + } +} diff --git a/app/Http/Resources/CurrentUserResource.php b/app/Http/Resources/CurrentUserResource.php index 34c43b24b..270b82e6b 100644 --- a/app/Http/Resources/CurrentUserResource.php +++ b/app/Http/Resources/CurrentUserResource.php @@ -12,7 +12,7 @@ /** * The signed-in user: their public profile, plus what only they see on the web's profile settings. That is the lane * or custom color they chose (`null` for Automatic) and the one Automatic gives them, their GitHub Sponsors page, and when their profile - * was last mirrored from GitHub. Their Remotes are `null` while turned off, as the web hides them, and otherwise say who + * was last mirrored from GitHub. The version of the terms in force, when they accepted, and whether it was this version, so the app knows to ask. Their Remotes are `null` while turned off, as the web hides them, and otherwise say who * can push to them, whether they share pulls, what the site lets them choose, and the unread count the web's nav shows. * * @mixin User @@ -36,6 +36,9 @@ public function toArray(Request $request): array 'automatic_lane' => $this->automaticLane(), 'sponsors_url' => $this->has_sponsors_listing ? "https://github.com/sponsors/{$this->username}" : null, 'github_synced_at' => $this->github_synced_at?->toIso8601ZuluString(), + 'terms_version' => config('legal.terms_version'), + 'terms_accepted_at' => $this->terms_accepted_at?->toIso8601ZuluString(), + 'terms_accepted' => $this->hasAcceptedCurrentTerms(), 'remotes' => $this->remotes_enabled && $user instanceof User ? [ 'from' => $this->remotes_from, 'pull_receipts' => $this->pull_receipts, diff --git a/app/Models/User.php b/app/Models/User.php index 085bb8f76..29d6ee1f4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -58,6 +58,8 @@ * @property-read CarbonInterface|null $github_synced_at * @property-read CarbonInterface|null $last_seen_at * @property-read CarbonInterface|null $banned_at + * @property-read CarbonInterface|null $terms_accepted_at + * @property-read string|null $terms_version * @property-read CarbonInterface|null $commit_rules_accepted_at * @property-read CarbonInterface $created_at * @property-read CarbonInterface $updated_at @@ -105,6 +107,8 @@ 'last_seen_at', 'banned_at', 'commit_rules_accepted_at', + 'terms_accepted_at', + 'terms_version', ])] final class User extends Authenticatable { @@ -145,6 +149,8 @@ final class User extends Authenticatable 'last_seen_at' => null, 'banned_at' => null, 'commit_rules_accepted_at' => null, + 'terms_accepted_at' => null, + 'terms_version' => null, ]; /** @@ -196,6 +202,7 @@ public function casts(): array 'last_seen_at' => 'date', 'banned_at' => 'datetime', 'commit_rules_accepted_at' => 'datetime', + 'terms_accepted_at' => 'datetime', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; @@ -365,6 +372,14 @@ public function getAuthPassword(): string return ''; } + /** + * Whether they have accepted the terms and privacy policy as they stand now. + */ + public function hasAcceptedCurrentTerms(): bool + { + return $this->terms_accepted_at !== null && $this->terms_version === config('legal.terms_version'); + } + public function isOnboarded(): bool { return $this->onboarded_at !== null; diff --git a/config/legal.php b/config/legal.php new file mode 100644 index 000000000..98c08298a --- /dev/null +++ b/config/legal.php @@ -0,0 +1,19 @@ + '2026-10-23', + +]; diff --git a/database/migrations/2026_10_23_120000_add_terms_acceptance_to_users_table.php b/database/migrations/2026_10_23_120000_add_terms_acceptance_to_users_table.php new file mode 100644 index 000000000..665dae4d6 --- /dev/null +++ b/database/migrations/2026_10_23_120000_add_terms_acceptance_to_users_table.php @@ -0,0 +1,22 @@ +timestamp('terms_accepted_at')->nullable(); + $table->string('terms_version', 10)->nullable(); + }); + } +}; diff --git a/resources/js/lib/legal.ts b/resources/js/lib/legal.ts index d7bca1214..905ee5346 100644 --- a/resources/js/lib/legal.ts +++ b/resources/js/lib/legal.ts @@ -1,6 +1,6 @@ -export const PRIVACY_EFFECTIVE_DATE = '20 October 2026'; +export const PRIVACY_EFFECTIVE_DATE = '23 October 2026'; -export const TERMS_EFFECTIVE_DATE = '20 October 2026'; +export const TERMS_EFFECTIVE_DATE = '23 October 2026'; export const COMPANY_NAME = 'Nuno Maduro, Unipessoal Lda'; diff --git a/resources/js/pages/privacy/show.tsx b/resources/js/pages/privacy/show.tsx index 93f09b6ae..32fc89e45 100644 --- a/resources/js/pages/privacy/show.tsx +++ b/resources/js/pages/privacy/show.tsx @@ -51,6 +51,15 @@ export default function Show() { listing, your public SSH key fingerprints, and who you follow and who follows you. , + <> + + From Apple, when you sign in with Apple in + the iOS app: + {' '} + your Apple account identifier, and the name and + email address you choose to share, which may be + Apple's private relay address. + , <> From the commit script, when you run it: @@ -92,6 +101,14 @@ export default function Show() { you use the service, such as the last day you used it. , + <> + From the iOS app: a key the app + makes on your phone to sign its requests, which + never leaves it, and the name of your phone. + The app asks for your photos or camera only + when you attach an image, and for your + microphone only when you record a voice note. + , <> Cookies and browser storage{' '} needed for the service to work, such as for @@ -104,8 +121,8 @@ export default function Show() { others give us about you, or that is generated when you use the service. New features may involve other data, which this policy covers once it is updated to describe - it. Signing in with GitHub is required to use the - service. + it. Signing in with GitHub, or with Apple in the iOS + app, is required to use the service.

@@ -211,7 +228,8 @@ export default function Show() {

You can export your data or delete your account from{' '} - settings. Under + settings, or + delete it in the iOS app under Settings. Under the GDPR, you may also have the right to access, correct, erase, restrict, or port your data, to object to its processing, and to lodge a complaint with a data diff --git a/resources/js/pages/terms/show.tsx b/resources/js/pages/terms/show.tsx index c5a04b546..c92bc2ea3 100644 --- a/resources/js/pages/terms/show.tsx +++ b/resources/js/pages/terms/show.tsx @@ -52,7 +52,7 @@ export default function Show() {

You must be at least 16 years old, sign in with your own - account, and be allowed to use the service under the + GitHub or Apple account, and be allowed to use the service under the laws that apply to you, including sanctions and export laws. If you use it for an organization, you accept these terms on its behalf. @@ -128,9 +128,15 @@ export default function Show() { ]} />

- To report content you believe is illegal or infringes - your rights, write to . We decide how to handle - reports. + There is no place on the service for objectionable + content or abusive users, and we have zero tolerance for + them. Report a commit with Report in its menu, and use + Mute and Block on a profile to stop seeing someone. To + report content you believe is illegal or infringes your + rights, or anything else, write to . We review + reports and act on content that breaks these rules + within 24 hours, by removing it and restricting or + banning the account that posted it.

@@ -147,9 +153,10 @@ export default function Show() {

If you break these terms, we may stop you from posting for a while (a timeout) or ban you. A ban applies to - your GitHub account, so you can't sign up again with it, + the GitHub or Apple account you signed in with, so you + can't sign up again with it, even after deleting your account. We keep a record of - timeouts and bans, and the GitHub account they apply to, + timeouts and bans, and the account they apply to, after your account is deleted.

@@ -172,6 +179,25 @@ export default function Show() {

+ +

+ If you use the iOS app, these terms are between you and + us only, not Apple. Apple is not responsible for the + app or its content, and has no obligation to provide + maintenance or support for it. If the app fails to + conform to a warranty, you may tell Apple, and Apple + will refund the purchase price, if any; Apple has no + other warranty obligation for the app. We, not Apple, + answer any claim about the app, including product + liability, legal or regulatory compliance, and consumer + protection. Apple and its subsidiaries are third-party + beneficiaries of these terms and may enforce them + against you once you accept them. You confirm that you + are not in a country under a U.S. government embargo or + listed as a restricted party. +

+
+

The service is provided "as is" and "as available", diff --git a/routes/api.php b/routes/api.php index c7c57ea7f..7e8cb2bf3 100644 --- a/routes/api.php +++ b/routes/api.php @@ -46,6 +46,7 @@ use App\Http\Controllers\Api\UserProfileController; use App\Http\Controllers\Api\UserRemotePreferencesController; use App\Http\Controllers\Api\UserRemotesController; +use App\Http\Controllers\Api\UserTermsController; use App\Http\Controllers\Cli\CommitController; use App\Http\Controllers\Cli\KeyLookupController; use App\Http\Middleware\AuthenticateSignedRequest; @@ -100,6 +101,7 @@ // Pastéis... Route::put('me/pasteis-mode', [UserPasteisModeController::class, 'update'])->name('api-user-pasteis-mode.update'); + Route::put('me/terms', [UserTermsController::class, 'update'])->name('api-user-terms.update'); // User Profile: mirrored from GitHub again, as Sync from GitHub does on the web... Route::put('me/github', [UserProfileController::class, 'update']) diff --git a/tests/Feature/Controllers/Api/CurrentUserControllerTest.php b/tests/Feature/Controllers/Api/CurrentUserControllerTest.php index d0a47dc28..38e4b6b4f 100644 --- a/tests/Feature/Controllers/Api/CurrentUserControllerTest.php +++ b/tests/Feature/Controllers/Api/CurrentUserControllerTest.php @@ -60,6 +60,9 @@ 'automatic_lane' => $user->automaticLane(), 'sponsors_url' => null, 'github_synced_at' => null, + 'terms_version' => config('legal.terms_version'), + 'terms_accepted_at' => null, + 'terms_accepted' => false, 'remotes' => [ 'from' => 'following', 'pull_receipts' => true, diff --git a/tests/Feature/Controllers/Api/DeviceControllerTest.php b/tests/Feature/Controllers/Api/DeviceControllerTest.php index b4b018dca..b72c8f48e 100644 --- a/tests/Feature/Controllers/Api/DeviceControllerTest.php +++ b/tests/Feature/Controllers/Api/DeviceControllerTest.php @@ -26,6 +26,26 @@ )); }); +it('records the terms as accepted when the app says which version it showed before sign-in', function (): void { + signedApiRequest($this->keyPair, 'POST', route('api-device.store'), ['code' => $this->code, 'code_verifier' => $this->verifier, 'terms_version' => config('legal.terms_version')]) + ->assertOk() + ->assertJsonPath('data.terms_accepted', true); + + expect($this->user->refresh()->hasAcceptedCurrentTerms())->toBeTrue(); +}); + +it('leaves the terms alone when the app does not say, and refuses a version that is not in force', function (): void { + signedApiRequest($this->keyPair, 'POST', route('api-device.store'), ['code' => $this->code, 'code_verifier' => $this->verifier, 'terms_version' => '1999-01-01']) + ->assertUnprocessable() + ->assertJsonValidationErrors('terms_version'); + + signedApiRequest($this->keyPair, 'POST', route('api-device.store'), ['code' => $this->code, 'code_verifier' => $this->verifier]) + ->assertOk() + ->assertJsonPath('data.terms_accepted', false); + + expect($this->user->refresh()->terms_accepted_at)->toBeNull(); +}); + it('approves the key that redeems the code as a device, and answers like me', function (): void { $response = signedApiRequest($this->keyPair, 'POST', route('api-device.store'), ['code' => $this->code, 'code_verifier' => $this->verifier]); diff --git a/tests/Feature/Controllers/Api/UserTermsControllerTest.php b/tests/Feature/Controllers/Api/UserTermsControllerTest.php new file mode 100644 index 000000000..8119fec33 --- /dev/null +++ b/tests/Feature/Controllers/Api/UserTermsControllerTest.php @@ -0,0 +1,52 @@ +create(); + $keyPair = apiKeyPairFor($user); + + signedApiRequest($keyPair, 'GET', route('api-current-user.show')) + ->assertOk() + ->assertJsonPath('data.terms_version', config('legal.terms_version')) + ->assertJsonPath('data.terms_accepted_at', null) + ->assertJsonPath('data.terms_accepted', false); + + $user->update(['terms_accepted_at' => now(), 'terms_version' => '2020-01-01']); + + signedApiRequest($keyPair, 'GET', route('api-current-user.show'))->assertJsonPath('data.terms_accepted', false); +}); + +it('records that the terms were accepted, and answers as the current user endpoint does', function (): void { + Date::setTestNow('2026-10-23 12:00:00'); + + $user = User::factory()->create(); + $keyPair = apiKeyPairFor($user); + + $response = signedApiRequest($keyPair, 'PUT', route('api-user-terms.update'), ['version' => config('legal.terms_version')]); + + $response->assertOk() + ->assertJsonPath('data.terms_accepted', true) + ->assertJsonPath('data.terms_accepted_at', '2026-10-23T12:00:00Z') + ->assertExactJson(signedApiRequest($keyPair, 'GET', route('api-current-user.show'))->json()); + + expect($user->refresh()->hasAcceptedCurrentTerms())->toBeTrue() + ->and($user->terms_version)->toBe(config('legal.terms_version')); +}); + +it('only accepts the version in force, so an old build cannot accept terms it never showed', function (mixed $version): void { + $user = User::factory()->create(); + + signedApiRequest(apiKeyPairFor($user), 'PUT', route('api-user-terms.update'), $version === null ? [] : ['version' => $version]) + ->assertUnprocessable() + ->assertJsonValidationErrors('version'); + + expect($user->refresh()->terms_accepted_at)->toBeNull(); +})->with([null, '2020-01-01', '2026-10-22']); + +it('rejects requests that are not signed', function (): void { + $this->putJson(route('api-user-terms.update'), ['version' => config('legal.terms_version')])->assertUnauthorized(); +}); From e770927fa2c4d7585be486cb920d52ae5de21621 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Mon, 26 Oct 2026 21:17:27 +0000 Subject: [PATCH 2/2] refactor: derive the accepted terms version instead of storing it The version in force is a timestamp, and an acceptance at or after it is an acceptance of that version, so terms_accepted_at is the only column needed. --- app/Actions/UpdateUserTermsAcceptance.php | 5 +--- app/Models/User.php | 5 +--- config/legal.php | 7 +++--- ...00_add_terms_acceptance_to_users_table.php | 5 ++-- resources/js/pages/privacy/show.tsx | 16 ++++++------- resources/js/pages/terms/show.tsx | 23 +++++++++---------- .../Api/UserTermsControllerTest.php | 5 ++-- 7 files changed, 29 insertions(+), 37 deletions(-) diff --git a/app/Actions/UpdateUserTermsAcceptance.php b/app/Actions/UpdateUserTermsAcceptance.php index d28f23782..c356d1fa5 100644 --- a/app/Actions/UpdateUserTermsAcceptance.php +++ b/app/Actions/UpdateUserTermsAcceptance.php @@ -13,9 +13,6 @@ */ public function handle(User $user): void { - $user->update([ - 'terms_accepted_at' => now(), - 'terms_version' => config('legal.terms_version'), - ]); + $user->update(['terms_accepted_at' => now()]); } } diff --git a/app/Models/User.php b/app/Models/User.php index 29d6ee1f4..04bb6d6d1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -59,7 +59,6 @@ * @property-read CarbonInterface|null $last_seen_at * @property-read CarbonInterface|null $banned_at * @property-read CarbonInterface|null $terms_accepted_at - * @property-read string|null $terms_version * @property-read CarbonInterface|null $commit_rules_accepted_at * @property-read CarbonInterface $created_at * @property-read CarbonInterface $updated_at @@ -108,7 +107,6 @@ 'banned_at', 'commit_rules_accepted_at', 'terms_accepted_at', - 'terms_version', ])] final class User extends Authenticatable { @@ -150,7 +148,6 @@ final class User extends Authenticatable 'banned_at' => null, 'commit_rules_accepted_at' => null, 'terms_accepted_at' => null, - 'terms_version' => null, ]; /** @@ -377,7 +374,7 @@ public function getAuthPassword(): string */ public function hasAcceptedCurrentTerms(): bool { - return $this->terms_accepted_at !== null && $this->terms_version === config('legal.terms_version'); + return $this->terms_accepted_at?->gte(config()->string('legal.terms_version')) ?? false; } public function isOnboarded(): bool diff --git a/config/legal.php b/config/legal.php index 98c08298a..66cbe1478 100644 --- a/config/legal.php +++ b/config/legal.php @@ -9,11 +9,12 @@ | Terms version |-------------------------------------------------------------------------- | - | The version of the terms and privacy policy people accept, the date they took effect as YYYY-MM-DD. Keep it in step - | with the effective dates in resources/js/lib/legal.ts. Raising it asks everyone to accept again in the app. + | The version of the terms and privacy policy people accept: the moment it took effect, as an ISO 8601 UTC timestamp. + | Anyone who accepted at or after it accepted this version. Set a new version to the moment it ships, never earlier, + | and keep its date in step with resources/js/lib/legal.ts. Raising it asks everyone to accept again in the app. | */ - 'terms_version' => '2026-10-23', + 'terms_version' => '2026-10-23T00:00:00Z', ]; diff --git a/database/migrations/2026_10_23_120000_add_terms_acceptance_to_users_table.php b/database/migrations/2026_10_23_120000_add_terms_acceptance_to_users_table.php index 665dae4d6..9b0ebf498 100644 --- a/database/migrations/2026_10_23_120000_add_terms_acceptance_to_users_table.php +++ b/database/migrations/2026_10_23_120000_add_terms_acceptance_to_users_table.php @@ -9,14 +9,13 @@ return new class extends Migration { /** - * When someone accepted the terms and privacy policy, and which version. Everyone already here is left empty: the - * apps ask them to accept the first time they open them. + * When someone accepted the terms and privacy policy. The version they accepted is the one in force at that moment, + * so it is not stored. Everyone already here is left empty: the apps ask them to accept the first time they open them. */ public function up(): void { Schema::table('users', function (Blueprint $table): void { $table->timestamp('terms_accepted_at')->nullable(); - $table->string('terms_version', 10)->nullable(); }); } }; diff --git a/resources/js/pages/privacy/show.tsx b/resources/js/pages/privacy/show.tsx index 32fc89e45..bacf099c7 100644 --- a/resources/js/pages/privacy/show.tsx +++ b/resources/js/pages/privacy/show.tsx @@ -104,10 +104,10 @@ export default function Show() { <> From the iOS app: a key the app makes on your phone to sign its requests, which - never leaves it, and the name of your phone. - The app asks for your photos or camera only - when you attach an image, and for your - microphone only when you record a voice note. + never leaves it, and the name of your phone. The + app asks for your photos or camera only when you + attach an image, and for your microphone only + when you record a voice note. , <> Cookies and browser storage{' '} @@ -229,10 +229,10 @@ export default function Show() {

You can export your data or delete your account from{' '} settings, or - delete it in the iOS app under Settings. Under - the GDPR, you may also have the right to access, - correct, erase, restrict, or port your data, to object - to its processing, and to lodge a complaint with a data + delete it in the iOS app under Settings. Under the GDPR, + you may also have the right to access, correct, erase, + restrict, or port your data, to object to its + processing, and to lodge a complaint with a data protection authority, such as Portugal's CNPD. You may object at any time to processing based on our legitimate interests. To exercise these rights, write to . diff --git a/resources/js/pages/terms/show.tsx b/resources/js/pages/terms/show.tsx index c92bc2ea3..69e1f21f6 100644 --- a/resources/js/pages/terms/show.tsx +++ b/resources/js/pages/terms/show.tsx @@ -52,10 +52,10 @@ export default function Show() {

You must be at least 16 years old, sign in with your own - GitHub or Apple account, and be allowed to use the service under the - laws that apply to you, including sanctions and export - laws. If you use it for an organization, you accept - these terms on its behalf. + GitHub or Apple account, and be allowed to use the + service under the laws that apply to you, including + sanctions and export laws. If you use it for an + organization, you accept these terms on its behalf.

@@ -152,12 +152,11 @@ export default function Show() {

If you break these terms, we may stop you from posting - for a while (a timeout) or ban you. A ban applies to - the GitHub or Apple account you signed in with, so you - can't sign up again with it, - even after deleting your account. We keep a record of - timeouts and bans, and the account they apply to, - after your account is deleted. + for a while (a timeout) or ban you. A ban applies to the + GitHub or Apple account you signed in with, so you can't + sign up again with it, even after deleting your account. + We keep a record of timeouts and bans, and the account + they apply to, after your account is deleted.

@@ -182,8 +181,8 @@ export default function Show() {

If you use the iOS app, these terms are between you and - us only, not Apple. Apple is not responsible for the - app or its content, and has no obligation to provide + us only, not Apple. Apple is not responsible for the app + or its content, and has no obligation to provide maintenance or support for it. If the app fails to conform to a warranty, you may tell Apple, and Apple will refund the purchase price, if any; Apple has no diff --git a/tests/Feature/Controllers/Api/UserTermsControllerTest.php b/tests/Feature/Controllers/Api/UserTermsControllerTest.php index 8119fec33..b86578908 100644 --- a/tests/Feature/Controllers/Api/UserTermsControllerTest.php +++ b/tests/Feature/Controllers/Api/UserTermsControllerTest.php @@ -15,7 +15,7 @@ ->assertJsonPath('data.terms_accepted_at', null) ->assertJsonPath('data.terms_accepted', false); - $user->update(['terms_accepted_at' => now(), 'terms_version' => '2020-01-01']); + $user->update(['terms_accepted_at' => '2026-10-22 23:59:59']); signedApiRequest($keyPair, 'GET', route('api-current-user.show'))->assertJsonPath('data.terms_accepted', false); }); @@ -33,8 +33,7 @@ ->assertJsonPath('data.terms_accepted_at', '2026-10-23T12:00:00Z') ->assertExactJson(signedApiRequest($keyPair, 'GET', route('api-current-user.show'))->json()); - expect($user->refresh()->hasAcceptedCurrentTerms())->toBeTrue() - ->and($user->terms_version)->toBe(config('legal.terms_version')); + expect($user->refresh()->hasAcceptedCurrentTerms())->toBeTrue(); }); it('only accepts the version in force, so an old build cannot accept terms it never showed', function (mixed $version): void {