
Postoji jedan trenutak u životu svakog mobile developera koji prvi put paketira web aplikaciju s Capacitorom. Sve radi savršeno. Login, register, password reset — sve teče besprijekorno. A onda dodaš Google OAuth.
I odjednom: korisnik klikne "Prijava preko Googlea", Chrome se otvara, korisnik bira account, klikne Allow, vraća se nazad u aplikaciju... i ništa. Ekran prazan. Session nikad ne stigne. Aplikacija nema pojma da je korisnik upravo autentificiran.
Sat vremena gledao u kod. Tri dana gledao u stack traces. Rješenje je bilo jedan red u capacitor.config.ts — ali da bismo došli do njega, morali smo prvo razumjeti kako Capacitor Android, Chrome, Supabase Auth i custom URL scheme zapravo komuniciraju.
Ovo je priča o tom debug-u.
💡 TL;DR: Ako gradiš Capacitor Android aplikaciju s Supabase OAuth-om i ne radi ti redirect natrag u aplikaciju, vjerojatno ti fali custom URL scheme registriran u AndroidManifest.xml i ispravna redirectTo URL formula u supabase.auth.signInWithOAuth(). Sve detalje imaš dolje.
Crolingo, naša Croatian-language-for-foreign-workers SaaS aplikacija, gradi se kao Next.js web aplikacija. Za mobilnu verziju, koristimo Capacitor — bridge koji web aplikaciju paketira kao native iOS/Android binary. Brzo razvojno iskustvo, jedan codebase, dva platforma. Idealno za studije naše veličine.
Web verzija OAuth-a radila je iz dana u dan. Capacitor verzija — katastrofa.
Sekvenca koja je padala:
Korisnik otvori Crolingo Android aplikaciju
Klikne "Prijava preko Googlea"
Aplikacija poziva supabase.auth.signInWithOAuth({ provider: 'google' })
Otvori se Chrome (ne in-app browser, već stvarni Chrome) s Google login stranicom
Korisnik bira account, klikne Allow
Google redirecta na https://crolingo.com/auth/callback?code=...
Naša web stranica primi callback, exchange-a code za session, postavi cookie
I tu sve umre.
Korisnik je sada ulogiran u Chrome browseru na crolingo.com. Ali aplikacija je Capacitor binary koji ne dijeli cookies s Chrome-om. Aplikacija sjedi u background-u i čeka. Korisnik klikne natrag na ikonu aplikacije — ekran prazan, kao da se ništa nije dogodilo.
⚠️ Klasična zamka: Capacitor WebView i sistemski Chrome browser su dva potpuno odvojena WebView konteksta. Cookies, localStorage, IndexedDB — ništa se ne dijeli između njih. Što se događa u Chrome-u ostaje u Chrome-u.
Prirodno smo počeli s najjednostavnijim pretpostavkama. Svaka od njih nas je odvela u krug.
Pokušaj 1: Promijenili smo redirectTo u signInWithOAuth poziv da pokazuje na sebe — redirectTo: window.location.origin. Rezultat: Chrome i dalje preuzima, jer window.location unutar Capacitor WebView-a NIJE realan URL nego https://localhost.
Pokušaj 2: Probali smo Capacitor.Browser plugin koji otvara in-app browser tab. Bolje, ali Google blokira OAuth iz Browser plugina jer to izgleda kao embedded WebView (security policy).
Pokušaj 3: Pokušali smo capturati URL kroz App.addListener('appUrlOpen', ...) — Capacitor API za hvatanje deep linkova. Listener se nikad nije triggerao jer aplikacija nije bila registrirana za primanje deep linkova.
Pokušaj 4: Sumnja je pala na custom URL scheme. Probali smo dodati com.mayntech.crolingo://callback kao redirect URL. Google je vratio error — taj URL nije validan HTTPS endpoint.
Tri dana, četiri pokušaja, nula napretka. Vrijeme za sustavan debug.
Da bismo riješili problem, morali smo prvo razumjeti stvarno što se događa između koraka 6 i 7 gore. Evo dijagrama koji smo nacrtali na bijeloj ploči:

OAuth flow u Capacitor native aplikaciji ima četiri kritične točke koje moraju savršeno funkcionirati zajedno:
Polazak iz aplikacije — signInWithOAuth mora otvoriti sistemski browser (Chrome), ne in-app WebView. Inače Google blokira.
Google autentikacija — odvija se u Chrome browseru, normalno.
Redirect natrag — Google šalje code na callback URL. Taj URL mora biti vlasništvo aplikacije, ne web stranice. Inače code ostane u browseru.
Code exchange — aplikacija mora pokupiti code, poslati ga Supabase-u, dobiti session, spremiti ga lokalno.
Naš problem bio je u koraku 3. Imali smo callback URL koji je vodio na https://crolingo.com/auth/callback. To je web URL, ne aplikacijski URL. Browser ga je obrađivao kao bilo koju drugu stranicu — otvorio je u browseru i tamo ostao.
Pravo rješenje sastoji se od tri komada koji moraju biti savršeno usklađeni. Evo svakog.
U android/app/src/main/AndroidManifest.xml, u <activity> blok glavne aktivnosti, dodali smo intent-filter koji govori Android operativnom sustavu: "Ako netko otvori link s ovim scheme-om, otvori moju aplikaciju."
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="com.mayntech.crolingo" />
</intent-filter>Ovo je ključni dio. android:scheme="com.mayntech.crolingo" registrira našu aplikaciju kao handler za sve URL-ove koji počinju s com.mayntech.crolingo://. Sad operativni sustav zna: kad god neki link s tim prefixom dođe — pošalji ga u Crolingo.
capacitor.config.tsU Capacitor konfiguraciji, dodali smo server.androidScheme opciju.
const config: CapacitorConfig = {
appId: 'com.mayntech.crolingo',
appName: 'Crolingo',
webDir: 'out',
server: {
androidScheme: 'https',
hostname: 'app.crolingo.com'
}
};androidScheme: 'https' mijenja Capacitor-ov interni protokol s capacitor:// na https://. To je važno jer Supabase Auth ima zaštitu protiv non-HTTPS redirect URL-ova — ako pokušaš capacitor://callback, dobiješ error.
hostname: 'app.crolingo.com' znači da unutar aplikacije, window.location.host vraća app.crolingo.com umjesto localhost. To čisti puno bug-ova s relative URL-ovima.
Najzad, u našoj React komponenti za Google login button:
import { Capacitor } from '@capacitor/core';
import { App } from '@capacitor/app';
async function handleGoogleLogin() {
const isNative = Capacitor.isNativePlatform();
const redirectTo = isNative
? 'com.mayntech.crolingo://auth/callback'
: `${window.location.origin}/auth/callback`;
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo,
skipBrowserRedirect: isNative, // ne ostavi web redirect ako smo u native
},
});
if (isNative && data?.url) {
// Otvori sistemski browser
await Browser.open({ url: data.url });
}
}
// Slušaj povratak iz browsera
App.addListener('appUrlOpen', async ({ url }) => {
if (url.startsWith('com.mayntech.crolingo://auth/callback')) {
const params = new URL(url).searchParams;
const code = params.get('code');
if (code) {
await supabase.auth.exchangeCodeForSession(code);
// Sad smo ulogirani
}
await Browser.close();
}
});Tri ključne odluke ovdje:
redirectTo se mijenja po platformi. Web verzija ide na https://, native ide na custom scheme.
skipBrowserRedirect: true za native — Supabase ne pokušava sam redirectati, već nam vraća URL koji mi sami otvaramo kroz Browser.open().
appUrlOpen listener je magic. To je trenutak kad Android primijeti com.mayntech.crolingo://... URL i okida event u aplikaciji. Tu hvatamo code i razmjenimo ga za session.
Sustavan debug umjesto slijepih pokušaja. Prva tri dana pokušavali smo nasumično. Tek kad smo sjeli pred bijelu ploču i nacrtali stvarni flow, postalo je jasno gdje sjeda greška.
Custom scheme umjesto Universal Links. Universal Links zvuče elegantnije (https URL-ovi koji se otvaraju u aplikaciji), ali zahtijevaju Apple/Google verifikaciju domene, server-side fajlove i puno više moving parts. Custom scheme je manje "lijep" ali radi odmah.
Krenuli bismo s Capacitor dokumentacijom na deep linking od prvog dana. Sva tri dana debug-a mogli smo izbjeći da smo prvo pročitali Capacitor docs sekciju "App URLs" prije nego što smo pokušali integraciju.
Testirali bismo na fizičkom uređaju prije Internal Testing kanala. Emulator je u nekim slučajevima krio pravo ponašanje OAuth flow-a jer nije imao Chrome instaliran ispravno.
Ako gradiš Capacitor aplikaciju s OAuth-om, evo brza checklista koju bismo dali samima sebi prije tri dana:
Registriraj custom URL scheme u AndroidManifest.xml od prvog dana
Za iOS, ekvivalentna registracija ide u Info.plist (CFBundleURLSchemes)
U capacitor.config.ts postavi androidScheme: 'https' ako koristiš Supabase ili bilo koji OAuth provider s HTTPS zahtjevom
U OAuth provider dashboardu (Google Cloud Console), dodaj custom scheme URL kao authorized redirect URI
U Supabase dashboardu (Authentication → URL Configuration → Redirect URLs), dodaj custom scheme URL u allowed list
U React komponenti, detect native platform i mijenjaj redirectTo dinamički
Implementiraj App.addListener('appUrlOpen') za hvatanje povratka u aplikaciju
Testiraj na fizičkom uređaju, ne samo emulator-u
✅ Rezultat: OAuth flow u Crolingo Android aplikaciji sad radi end-to-end. Korisnik klikne login, Chrome se otvori, autentikacija prođe, Chrome se zatvori, aplikacija prima session — sve u manje od 5 sekundi. Bez vidljivih prekida, bez praznih ekrana, bez zbunjenosti.
Ovo je bio jedan od dva preostala kritična bug-a u Crolingo Android internal testing fazi. Drugi — Service Worker cache koji je usporavao prvi load aplikacije — riješili smo dva dana kasnije s next-pwa plugin-om. Pisat ćemo o tome zasebno.
Ako gradiš sličnu aplikaciju i zapneš na nekom dijelu OAuth flow-a, ili imaš drukčije rješenje za neki od koraka — voljeli bismo čuti. Najbolje rješenje često dolazi iz razgovora s nekim tko je pao u istu zamku.
Trebaš pomoć s projektom?
Razgovaraj s nama — direktno, bez sales pitcha.