Skip to content
Open
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
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@

A userscript fork of [Open-In-Apollo](https://github.com/AnthonyGress/Open-In-Apollo) by Anthony Gress to support Apollo running within [Live Container](https://github.com/AnthonyGress/Open-In-Apollo). Supports Live Container **only**, for normal sideloaded Apollo see original script.

I've also adapted this to a few other scripts for Twitter, Youtube, Instagram and uploaded here. Thanks to https://gist.github.com/ryuya0124/e6e59ceba8f03bf40f3b5accedc52e44 for Twitter script.
I've also adapted this to a few other scripts for Twitter, Youtube, Instagram, Spotify, Strava/Fitbod and uploaded here. Thanks to https://gist.github.com/ryuya0124/e6e59ceba8f03bf40f3b5accedc52e44 for Twitter script.

## Downloads
## Available Userscripts

- Safari extension to run user script (free): https://apps.apple.com/us/app/userscripts/id1463298887
### Strava ↔ Fitbod OAuth (Featured)
- **[Strava to Fitbod OAuth Live Container](strava-fitbod-oauth-live-container.user.js)** - Enables Strava authorization within Fitbod in Live Container
- Intercepts OAuth flow and redirects directly to Live Container
- No intermediate web pages or 404 errors
- Seamless integration experience

- Userscript code: [Open In Apollo Live Container Userscript](https://github.com/nathandaven/Open-In-Apollo-Live-Container/raw/525f547ed2cbca1d8edaeee8a9a8c52715521224/open-in-apollo-live-container.user.js)
### Other App Scripts
- [Open In Apollo Live Container](https://github.com/nathandaven/Open-In-Apollo-Live-Container/raw/525f547ed2cbca1d8edaeee8a9a8c52715521224/open-in-apollo-live-container.user.js) - Reddit links
- [Open In Twitter Live Container](open-in-twitter-live-container.user.js) - Twitter/X links
- [Open In Instagram Live Container](open-in-instagram-live-container.user.js) - Instagram links
- [Open In YouTube Live Container](open-in-youtube-live-container.user.js) - YouTube links
- [Open In Spotify Live Container](open-in-spotify-live-container.user.js) - Spotify links

## How to install

Expand All @@ -29,4 +38,22 @@ I've also adapted this to a few other scripts for Twitter, Youtube, Instagram an

- Tested on iOS 18.2.1, with Apollo 1.15.11 + ApolloPatcher 0.0.9 and Live Container 3.1.59-release

### Strava ↔ Fitbod OAuth Connection

**What it does**: Enables linking your Strava account to Fitbod when running inside Live Container.

**Why it's needed**: Fitbod uses deep links (`fitbod://`) for OAuth callbacks, but Safari can't open these URLs directly. This script intercepts the authorization flow and redirects to Live Container instead.

**How to use**:
1. Install the [Strava OAuth userscript](strava-fitbod-oauth-live-container.user.js)
2. In Fitbod (within Live Container), go to Settings → Link Strava
3. Click "Authorize" on the Strava page
4. Script automatically captures the OAuth response and opens Fitbod
5. Your Strava account is now linked!

**Technical flow**:
1. Intercepts form submission on Strava OAuth page
2. Submits authorization request via fetch
3. Captures the `fitbod://` redirect URL from response headers
4. Converts to `livecontainer://open-web-page?url=...`
5. Redirects directly to Fitbod in Live Container
111 changes: 111 additions & 0 deletions strava-fitbod-oauth-live-container.user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// ==UserScript==
// @name Strava to Fitbod OAuth Live Container
// @version 1.0.0
// @author AnasHafsi
// @description Intercepts Strava OAuth and redirects directly to Fitbod in Live Container
// @match *://www.strava.com/oauth/*
// @match *://www.strava.com/oauth/authorize*
// @match *://www.strava.com/oauth/mobile/authorize*
// @downloadURL https://github.com/nathandaven/Open-In-Live-Container/raw/refs/heads/main/strava-fitbod-oauth-live-container.user.js
// @updateURL https://github.com/nathandaven/Open-In-Live-Container/raw/refs/heads/main/strava-fitbod-oauth-live-container.user.js
// @homepage https://github.com/nathandaven/Open-In-Apollo-Live-Container/tree/main
// @grant none
// ==/UserScript==

(function () {
'use strict';
function utf8_to_b64(str) {
return window.btoa(unescape(encodeURIComponent(str)));
}

function extractRedirectUri(formAction) {
const match = formAction.match(/redirect_uri=([^&]+)/);
if (match) {
return decodeURIComponent(match[1]);
}
return null;
}

// Intercept form submission and redirect directly to Live Container
function interceptForm() {
try {
const form = document.querySelector('form[action*="redirect_uri"]');
if (!form) {
console.log('No authorization form found');
return false;
}

console.log('Found authorization form');

const formAction = form.getAttribute('action');
const redirectUri = extractRedirectUri(formAction);

if (!redirectUri || !redirectUri.includes('fitbod://')) {
console.log('Not a Fitbod OAuth flow, skipping');
return false;
}

form.addEventListener('submit', async function (e) {
e.preventDefault();

const formData = new FormData(form);
const actionUrl = new URL(form.action, window.location.origin);

const params = new URLSearchParams(actionUrl.search);
for (const [key, value] of formData.entries()) {
params.set(key, value);
}

try {
const response = await fetch(actionUrl.pathname + '?' + params.toString(), {
method: 'POST',
credentials: 'include',
redirect: 'manual'
});
const location = response.headers.get('Location');

if (location) {
if (location.startsWith('fitbod://')) {
// Convert to Live Container URL
const liveContainerUrl = `livecontainer://open-web-page?url=${utf8_to_b64(location)}`;
window.location.href = liveContainerUrl;
} else if (location.startsWith('https://fitbod.me')) {
// Fallback: convert https to fitbod:// then to Live Container
const fitbodUrl = location.replace('https://', 'fitbod://');
const liveContainerUrl = `livecontainer://open-web-page?url=${utf8_to_b64(fitbodUrl)}`;
window.location.href = liveContainerUrl;
} else {
console.error('Unexpected redirect URL:', location);
alert('Unexpected OAuth redirect. Check console for details.');
}
} else {
alert('OAuth authorization failed - no redirect found');
}
} catch (error) {
console.error('Error during OAuth submission:', error);
alert('Error during authorization: ' + error.message);
}
});
return true;

} catch (error) {
console.error('Error setting up form interceptor:', error);
return false;
}
}

// Run when DOM is ready
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', interceptForm);
} else {
interceptForm();
}

// Retry a few times in case of dynamic loading
setTimeout(interceptForm, 500);
setTimeout(interceptForm, 1000);
}

init();
})();