Skip to main content

Mobile deep dive

This page goes one layer below the Mobile app walkthrough: how the native iOS and Android apps actually behave, what's different from the web, and the platform-specific quirks worth knowing.

If you're just getting set up, start with Mobile app first.

Pull-to-refresh

Drag down at the top of a scrollable screen (dashboard, members list, forums, etc.) to refresh the data on it.

  • Native (iOS and Android). The pull gesture is intercepted, a circular indicator slides into view, and on release past the threshold the entire React Query cache is invalidated — the screen re-fetches its data.
  • Web. Pull-to-refresh is deliberately disabled on web. Earlier versions of the app shipped a web version that tried to claim vertical drag gestures and ended up blocking the page's native scroll, especially on mobile dashboards. The fix is to no-op on web and let the browser handle its own scroll. Chrome on Android has its own pull-to-refresh; Safari doesn't, and that's expected.

The threshold is ~80px of pull. You'll see the refresh icon fade in as you cross it.

Push notifications and the token registration UX

The first time you open the native app and sign in, the app:

  1. Checks the OS-level notification permission for the app.
  2. If permission is prompt, the OS shows the standard "GreekManage Would Like to Send You Notifications" dialog. Tap Allow.
  3. The app registers with APNs (iOS) or FCM (Android) and silently posts the device token to the server.
  4. Future pushes flow to your device based on what you've opted into in Notifications → Preferences.

If you tap Don't Allow, the registration step is skipped. The app keeps working, and you can re-enable notifications from your phone's Settings later — but the app does not re-prompt automatically.

There's no in-app feedback when token registration succeeds — it happens in the background. You'll know it worked when you actually receive a push notification. If pushes never arrive, check:

  • The OS-level notification toggle for GreekManage.
  • Your preferences page (the channels for the events you care about must be opted in).
  • That you're still authenticated (the token registers on first authenticated session).

Biometric unlock vs. passkey sign-in

These two things sound similar but are doing very different work. Disambiguating:

Biometric unlockPasskey sign-in
What it doesRe-opens your existing session with Face ID / Touch ID / fingerprintEstablishes a new authenticated session from scratch
When it triggersEvery time you open the app after enabling itOn the sign-in screen, when you tap Sign in with a passkey
Works on web?No (native only)Yes (any WebAuthn-capable browser)
Replaces password?No — your session was already authenticated; biometric just unlocks itYes — passkey is your credential
Where it's enabledAccount Settings on mobileAccount Settings → Passkeys
If it failsFalls back to password sign-inFalls back to email / password

In other words: passkey is how you sign in; biometric is how you re-open the app without re-typing anything once you've signed in once.

Both can use the same hardware (the iPhone's Face ID or your Android's fingerprint sensor) — but a passkey is bound cryptographically to your account on the server, while biometric unlock is purely a local convenience and stores no credentials with the platform.

Offline behavior

The native app shows a red No internet connection banner across the top when your device is offline. Practical scope:

  • Reads are cached. Pages you've already loaded keep showing whatever data they had cached locally. React Query holds responses in memory for the session.
  • Writes fail. Anything that hits the network — sending a message, posting a comment, submitting a compliance form, paying an invoice — returns an error until you're back online.
  • There is no offline write queue. Actions don't get spooled and sent later; they fail immediately. You'd retry manually once you reconnect.
  • No offline-only mode. The app does not have a "cached / read-only" mode. Reads work because the data happens to be in memory, not because the app has a true offline data layer.

The banner disappears automatically when the OS reports connectivity has come back.

What the native app does that the web doesn't

  • Pull-to-refresh on scrollable screens (described above).
  • Biometric unlock on app open (opt-in).
  • Push notifications routed through APNs / FCM.
  • Connection-loss banner.
  • Native keyboard handling — keyboard show / hide events toggle a body class so the layout shrinks instead of getting pushed up.
  • Splash screen on app launch (hidden once the app is ready).
  • Status bar styling matches the app's color scheme.

Everything else (the actual screens, data, forms, navigation) is the same web app rendered inside a Capacitor webview.

iOS-specific behavior

  • Status bar style. The native shell sets a light-content status bar by default. The home indicator area is respected automatically by the safe-area insets in the CSS.
  • Face ID / Touch ID. Triggered through Capacitor's biometric plugin. The OS controls the prompt UI — the app just initiates it.
  • Splash screen is the native iOS launch storyboard. It's hidden as soon as the JS bundle has loaded enough to render the first screen.
  • TestFlight vs. App Store. Behavior is identical. TestFlight builds are signed for internal / beta distribution; App Store builds for production. Same Capacitor wrapper either way.

Android-specific behavior

  • Hardware back button. Pressing the system back button navigates back in the in-app history. When there's no history to go back to (you're on the root screen), the app minimizes instead of closing — same as most native Android apps.
  • Keyboard resize mode. On Android, the keyboard plugin's setResizeMode call is a no-op (the API isn't implemented), but the app still listens for keyboardWillShow / keyboardWillHide events and applies the keyboard-open class to the body. That's enough to drive layout adjustments via CSS.
  • Mixed content. The native shell uses CapacitorHttp to bypass the webview's mixed-content blocking, which matters when the staging stack is on HTTPS but a tunnel rewrites URLs. End users don't see this; it's a deployment detail.
  • Adaptive icons. The launcher icon is configured as an adaptive icon (foreground + background layers) so the home screen icon respects the user's OS-level icon shape (circle / squircle / rounded square).
  • Play Store internal testing. Distribution typically goes through Play internal testing tracks. Same APK as production internally; once promoted, it ships through the public Play Store track.

App version vs. web version

The native and web codebases are the same React app. There are no native-only screens, no web-only features beyond what's listed above. When your org pushes a backend update:

  • Web users get it on next page load.
  • Native users get it the next time they open the app and the bundled JS starts up; for any feature that requires a backend change, both see it at the same time.

The native shell itself only changes when a new build is uploaded to TestFlight / App Store / Play Store — typically when Capacitor plugins are upgraded or platform-level fixes are made. The app does not enforce "you must update to v0.X" today; you can stay on an older bundled version of the native app until you choose to update it.

Errors and edge cases

  • Biometric unlock fails repeatedly. The OS falls back to the device passcode after the configured number of failed attempts. You can also disable biometric unlock in Account Settings and use password sign-in instead.
  • Passkey not recognized in the native app. Confirm the passkey was registered against app.greekmanage.com (the iOS Associated Domain and Android Asset Links are configured for that origin). Passkeys registered against an old domain won't be offered.
  • Pull-to-refresh feels stuck on web mobile. It's expected — pull-to-refresh is off on web. Refresh the page through your browser's reload affordance instead.
  • Push token registration fails silently. The app retries on next authenticated launch. If pushes don't arrive after a couple of launches, force-quit and reopen the app, then check OS settings.

Last verified against v0.62.1 (2026-05-11).