External links, sign-in flows and app addresses
SDK 1.5 (olan.ui.openUrl, olan.ui.openExternal, permanent app addresses). No permission is
needed.
olan.ui.openUrl(url) #
Opens a page in the platform browser and resolves at once: a new tab on the web, the default browser on desktop, the system browser on iOS and Android.
await olan.ui.openUrl('https://media.example.com/items/7');
olan.ui.openExternal(url, { expectReturn? }) #
For flows that come back to the app: connecting an account, OAuth of another service, a payment
page. It opens the system browser, never an embedded web view: a new tab on the web, the default
browser on desktop, ASWebAuthenticationSession on iOS, Custom Tabs on Android.
const result = await olan.ui.openExternal(authorizeUrl, { expectReturn: true });
if (result.returned) {
// result.params: the return address's query, e.g. { state: 'st_91', connection: 'connected' }
await olan.remote.request({ method: 'POST', path: '/connections/complete', body: result.params });
}
- Result:
{ returned: true, params: Record<string, string> }or{ returned: false }. expectReturndefaults tofalse: the call resolves{ returned: false }as soon as the page opens.- With
expectReturn: trueit resolves when the flow reaches the app's return address, or{ returned: false }when the person closes it (iOS cancel), anotheropenExternalof the same installation starts (one pending flow per installation) or 30 minutes pass. - URL rule (also for
openUrl):https:;http:only for loopback and private addresses in development installations; no credentials. Other schemes:invalid_params. - When the host cannot tie the call to the person's tap (a pop-up blocker on the web), it asks first ("Bağlantı açılsın mı?").
The return address #
https://<webOrigin>/apps/return/<installationId>?state=<your state>&<other params>
-
webOriginis the Olan web app's origin (https://olan.appwhile the API ishttps://api.olan.app), published in the issuer's/.well-known/olan(below). -
Your server builds it and puts it in the flow (e.g. OAuth
redirect_uriof your own callback, which then redirects here):// in a handleRequest route (remote-requests.md) 'POST /connections/start': async (ctx) => { const state = await saveState({ installationId: ctx.installationId, userId: ctx.user!.id }); const returnUrl = await olan.appReturnUrl({ issuer: ctx.issuer, installationId: ctx.installationId }); return { body: { authorizeUrl: `https://media.example.com/connect?state=${state}&return=${encodeURIComponent(returnUrl)}` } }; },olan.appReturnUrl({ issuer, installationId })(@olan-app/sdk/server) readswebOriginfrom the issuer's/.well-known/olan(cached; the issuer must be inissuers; an Olan server before 1.5 has none:web_origin_unknown).appReturnUrl(webOrigin, installationId)of@olan-app/schemabuilds it when you already know the origin. -
When something else gives you a return URL (a client, a query parameter), accept it only when
await olan.isAppReturnUrl({ issuer, installationId, url })is true: same origin, exactly/apps/return/<installationId>, any query, no credentials. -
stateis yours: create it on your server, check it when the flow comes back. Olan passes it through untouched. -
params: the query parameters as strings; the last of repeated keys wins; at most 50 keys and 4 KiB; the fragment (#…) is ignored. Never put secrets in them.
Coming back without a pending call #
If no openExternal is waiting (the app was closed, the tab reloaded, the phone killed the app),
Olan opens the app's app surface with the parameters as params.return:
function Home() {
const returned = useSurfaceParams<'app'>()?.return; // { state, connection, … } or undefined
useEffect(() => { if (returned?.state) void completeConnection(returned); }, [returned?.state]);
…
}
On the web the return page hands the parameters to the waiting tab (BroadcastChannel) and
closes itself; without a waiting tab it opens /apps/<installationId> with return. On iOS and
Android the return address is a universal link / App Link that brings Olan to the front.
App addresses: /apps/a/<appId> #
Every app has a permanent address, the same for every organization and installation:
https://<webOrigin>/apps/a/<appId>[/<screen>]?<params>
https://olan.app/apps/a/com.example.media-desk/item?id=item_7
- Olan finds the person's installation of
appIdand opens itsappsurface (not installed: a link to the store page; plan without the app: "planınızda yok"). On iOS and Android it opens Olan. screen(^[A-Za-z0-9_-]{1,64}$, defaultindex) and the query (strings, ≤ 50 keys, ≤ 4 KiB) become the start of the navigation stack (init.navigation):index: the first screen'suseNavigation().paramsare the query parameters;- another screen: the stack is
[index, <screen>]with the params on<screen>; the back control returns to the first screen.
function App() {
const { screen, params } = useNavigation();
const p = (params ?? {}) as { id?: string };
if (screen === 'item' && p.id) return <ItemScreen id={p.id} />; // /apps/a/<appId>/item?id=…
return <Home />;
}
Use it in emails, chat cards (open buttons go to the app anyway), notifications and your own
web pages. appLinkUrl(webOrigin, appId, { screen?, params? }) and parseAppLink(url) of
@olan-app/schema build and read them. Test with renderApp({ navigation: { screen: 'item', params: { id: 'item_7' } } }).
/.well-known/olan (Olan 1.5) #
{
"webOrigin": "https://olan.app",
"apps": {
"issuer": "https://api.olan.app",
"returnUrlTemplate": "https://olan.app/apps/return/{installationId}",
"appUrlTemplate": "https://olan.app/apps/a/{appId}",
"features": ["…", "remote-requests", "files", "external-return", "app-links"]
}
}
features tells an app server what this Olan server supports; a server without remote-requests
answers olan.remote.request with unknown_method.
Testing #
renderApp({ ui: { openExternal: { returned: true, params: { state: 'st_1', connection: 'connected' } } } })
answers openExternal(…, { expectReturn: true }) (default { returned: false });
app.host.opened lists { url, external, expectReturn } of every opened URL;
renderApp({ params: { return: { state: 'st_1' } } }) opens the app surface as a return without a
pending call (testing.md#remote-requests-files-and-links).