From 3d240eca814308f19163f8ce58faccd2a4f47398 Mon Sep 17 00:00:00 2001 From: Leon Morival Date: Wed, 20 May 2026 16:56:42 +0200 Subject: [PATCH] feat: strava deeplink to mobile --- .env.example | 1 - .gitea/workflows/deploy.yml | 3 --- app/Http/Controllers/StravaController.php | 13 +++++++++++++ app/Services/StravaClient.php | 10 +++++++--- config/deploy.yml | 1 - config/services.php | 1 - routes/web.php | 5 +++++ tests/Feature/StravaConnectionTest.php | 18 +++++++++++++++--- 8 files changed, 40 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 6e499ec..e86e313 100644 --- a/.env.example +++ b/.env.example @@ -61,7 +61,6 @@ EXPO_ACCESS_TOKEN= STRAVA_CLIENT_ID= STRAVA_CLIENT_SECRET= -STRAVA_REDIRECT_URI= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 355f187..e41df13 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -50,7 +50,6 @@ jobs: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} STRAVA_CLIENT_ID: ${{ secrets.STRAVA_CLIENT_ID }} STRAVA_CLIENT_SECRET: ${{ secrets.STRAVA_CLIENT_SECRET }} - STRAVA_REDIRECT_URI: ${{ secrets.STRAVA_REDIRECT_URI }} run: | set -eu @@ -75,7 +74,6 @@ jobs: -e SSH_PRIVATE_KEY \ -e STRAVA_CLIENT_ID \ -e STRAVA_CLIENT_SECRET \ - -e STRAVA_REDIRECT_URI \ --entrypoint /bin/sh \ "$KAMAL_IMAGE" -lc ' set -eu @@ -115,7 +113,6 @@ jobs: SSH_PRIVATE_KEY=$SSH_PRIVATE_KEY STRAVA_CLIENT_ID=$STRAVA_CLIENT_ID STRAVA_CLIENT_SECRET=$STRAVA_CLIENT_SECRET - STRAVA_REDIRECT_URI=$STRAVA_REDIRECT_URI SECRETS chmod 600 .kamal/secrets diff --git a/app/Http/Controllers/StravaController.php b/app/Http/Controllers/StravaController.php index b32ddc0..40df113 100644 --- a/app/Http/Controllers/StravaController.php +++ b/app/Http/Controllers/StravaController.php @@ -8,6 +8,7 @@ use App\Http\Requests\StravaSyncRequest; use App\Models\WorkoutSessions; use App\Services\StravaClient; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; @@ -29,6 +30,18 @@ class StravaController extends Controller ]); } + public function redirectToMobileApp(Request $request): RedirectResponse + { + $params = collect($request->only(['code', 'error', 'scope', 'state'])) + ->filter(fn ($value): bool => $value !== null && $value !== '') + ->all(); + + $query = http_build_query($params, '', '&', PHP_QUERY_RFC3986); + $url = 'dailymeal://strava/callback'.($query ? "?{$query}" : ''); + + return redirect()->away($url); + } + public function callback(StravaCallbackRequest $request, StravaClient $strava): JsonResponse { $data = $request->validated(); diff --git a/app/Services/StravaClient.php b/app/Services/StravaClient.php index 3a6a751..8d52e46 100644 --- a/app/Services/StravaClient.php +++ b/app/Services/StravaClient.php @@ -15,15 +15,14 @@ class StravaClient public function authorizationUrl(string $state): string { $clientId = config('services.strava.client_id'); - $redirectUri = config('services.strava.redirect_uri'); - if (! $clientId || ! $redirectUri) { + if (! $clientId) { throw new RuntimeException('Strava is not configured.'); } return self::AUTHORIZE_URL.'?'.http_build_query([ 'client_id' => $clientId, - 'redirect_uri' => $redirectUri, + 'redirect_uri' => $this->redirectUri(), 'response_type' => 'code', 'approval_prompt' => 'auto', 'scope' => config('services.strava.scope', 'read,activity:read'), @@ -97,4 +96,9 @@ class StravaClient ->connectTimeout(3) ->retry([100, 500, 1000]); } + + private function redirectUri(): string + { + return rtrim((string) config('app.url'), '/').'/strava/callback'; + } } diff --git a/config/deploy.yml b/config/deploy.yml index 8d1a48c..3cdaf36 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -78,7 +78,6 @@ env: - MEILISEARCH_KEY - STRAVA_CLIENT_ID - STRAVA_CLIENT_SECRET - - STRAVA_REDIRECT_URI volumes: - bemeal_storage_data:/var/www/html/storage/app diff --git a/config/services.php b/config/services.php index 89aff53..a74b68c 100644 --- a/config/services.php +++ b/config/services.php @@ -29,7 +29,6 @@ return [ 'strava' => [ 'client_id' => env('STRAVA_CLIENT_ID'), 'client_secret' => env('STRAVA_CLIENT_SECRET'), - 'redirect_uri' => env('STRAVA_REDIRECT_URI'), 'scope' => env('STRAVA_SCOPE', 'read,activity:read'), ], diff --git a/routes/web.php b/routes/web.php index b3d9bbc..c6e4daf 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1 +1,6 @@ name('strava.web-callback'); diff --git a/tests/Feature/StravaConnectionTest.php b/tests/Feature/StravaConnectionTest.php index a70ce1c..bb9b927 100644 --- a/tests/Feature/StravaConnectionTest.php +++ b/tests/Feature/StravaConnectionTest.php @@ -10,13 +10,13 @@ use Laravel\Sanctum\Sanctum; uses(RefreshDatabase::class); beforeEach(function () { + config()->set('app.url', 'https://bemeal.leonmorival.com'); config()->set('services.strava.client_id', '248440'); config()->set('services.strava.client_secret', 'strava-secret'); - config()->set('services.strava.redirect_uri', 'dailymeal://strava/callback'); config()->set('services.strava.scope', 'read,activity:read'); }); -it('returns a strava authorization url for the mobile app', function () { +it('returns a strava authorization url for the api callback', function () { Sanctum::actingAs(User::factory()->create()); $response = $this->getJson('/api/strava/authorization-url'); @@ -30,13 +30,25 @@ it('returns a strava authorization url for the mobile app', function () { parse_str((string) parse_url($authorizationUrl, PHP_URL_QUERY), $query); expect($query['client_id'])->toBe('248440'); - expect($query['redirect_uri'])->toBe('dailymeal://strava/callback'); + expect($query['redirect_uri'])->toBe('https://bemeal.leonmorival.com/strava/callback'); expect($query['response_type'])->toBe('code'); expect($query['approval_prompt'])->toBe('auto'); expect($query['scope'])->toBe('read,activity:read'); expect($query['state'])->not->toBeEmpty(); }); +it('redirects the strava web callback to the mobile app', function () { + $query = http_build_query([ + 'code' => 'authorization-code', + 'scope' => 'read activity:read', + 'state' => 'oauth-state', + ], '', '&', PHP_QUERY_RFC3986); + + $this + ->get("/strava/callback?{$query}") + ->assertRedirect("dailymeal://strava/callback?{$query}"); +}); + it('stores strava tokens from an authorization code', function () { Http::preventStrayRequests(); Http::fake([