Skip to content
All use cases

One feedback loop across clients

Keep mobile requests connected to the same public plan.

PrioSmith documents iOS, Android, and Flutter web-view examples that open the hosted portal. Treat each as a starting point and test navigation, identity, and lifecycle behavior in your real app.

One connected workflow

Collect once. Keep the outcome visible.

  1. 01Open the hosted board

    Begin with the portal URL so mobile users reach the same project as web users.

  2. 02Adapt the client example

    Choose the documented iOS, Android, or Flutter starting point and test it in the real navigation stack.

  3. 03Share progress

    Use the public roadmap and changelog so submitted requests remain visible after collection.

Documented starting points

Integration examples, clearly scoped.

These snippets reflect examples already documented inside PrioSmith. They are not proof of production behavior in your client. Validate navigation, security, accessibility, and lifecycle handling before release.

iOS web view

Toolchain-checked integration example — validate lifecycle in your app

Swift
import Combine
import Foundation
import SwiftUI
import WebKit

@MainActor
private final class PrioSmithFeedbackNavigator: ObservableObject {
    weak var webView: WKWebView?

    @discardableResult
    func goBack() -> Bool {
        guard let webView, webView.canGoBack else { return false }
        webView.goBack()
        return true
    }
}

struct PrioSmithFeedbackView: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(\.openURL) private var openURL
    @StateObject private var navigator = PrioSmithFeedbackNavigator()

    var body: some View {
        PrioSmithFeedbackWebView(
            navigator: navigator,
            openExternalURL: { target in
                _ = openURL(target)
            }
        )
        .navigationBarBackButtonHidden()
        .toolbar {
            ToolbarItem(placement: .topBarLeading) {
                Button {
                    if !navigator.goBack() {
                        dismiss()
                    }
                } label: {
                    Label("Back", systemImage: "chevron.backward")
                }
                .accessibilityIdentifier("priosmith-feedback-back")
            }
        }
    }
}

private struct PrioSmithFeedbackWebView: UIViewRepresentable {
    private let portalURL = URL(string: "https://app.priosmith.example/p/PROJECT_SLUG/board")!
    let navigator: PrioSmithFeedbackNavigator
    let openExternalURL: (URL) -> Void

    func makeCoordinator() -> Coordinator {
        Coordinator(
            portalURL: portalURL,
            navigator: navigator,
            openExternalURL: openExternalURL
        )
    }

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView(frame: .zero)
        navigator.webView = webView
        webView.navigationDelegate = context.coordinator
        webView.allowsBackForwardNavigationGestures = true
        webView.load(URLRequest(url: portalURL))
        return webView
    }

    func updateUIView(_ webView: WKWebView, context: Context) {}

    static func dismantleUIView(_ webView: WKWebView, coordinator: Coordinator) {
        coordinator.detach(webView)
        webView.navigationDelegate = nil
    }

    @MainActor
    final class Coordinator: NSObject, WKNavigationDelegate {
        private let portalURL: URL
        private let navigator: PrioSmithFeedbackNavigator
        private let openExternalURL: (URL) -> Void

        init(
            portalURL: URL,
            navigator: PrioSmithFeedbackNavigator,
            openExternalURL: @escaping (URL) -> Void
        ) {
            self.portalURL = portalURL
            self.navigator = navigator
            self.openExternalURL = openExternalURL
        }

        func webView(
            _ webView: WKWebView,
            decidePolicyFor navigationAction: WKNavigationAction,
            decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void
        ) {
            if navigationAction.targetFrame?.isMainFrame == false {
                decisionHandler(.allow)
                return
            }

            guard let target = navigationAction.request.url else {
                decisionHandler(.cancel)
                return
            }

            if hasSameOrigin(target, portalURL) {
                decisionHandler(.allow)
                return
            }

            openExternalURL(target)
            decisionHandler(.cancel)
        }

        func detach(_ webView: WKWebView) {
            if navigator.webView === webView {
                navigator.webView = nil
            }
        }

        private func hasSameOrigin(_ target: URL, _ expected: URL) -> Bool {
            guard
                let targetScheme = target.scheme?.lowercased(),
                let expectedScheme = expected.scheme?.lowercased(),
                let targetHost = target.host?.lowercased(),
                let expectedHost = expected.host?.lowercased()
            else {
                return false
            }

            return targetScheme == expectedScheme &&
                targetHost == expectedHost &&
                effectivePort(target) == effectivePort(expected)
        }

        private func effectivePort(_ url: URL) -> Int? {
            if let port = url.port { return port }
            switch url.scheme?.lowercased() {
            case "http": return 80
            case "https": return 443
            default: return nil
            }
        }
    }
}

Android web view

Toolchain-checked integration example — validate lifecycle in your app

Kotlin
import android.annotation.SuppressLint
import android.net.Uri
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient

private val prioSmithOrigin = Uri.parse("https://app.priosmith.example")

// Open external links, including sign-in, with a browser or Custom Tab.
@SuppressLint("SetJavaScriptEnabled")
fun openPrioSmithFeedback(
    webView: WebView,
    openExternalUrl: (Uri) -> Unit,
) {
    webView.settings.javaScriptEnabled = true
    webView.webViewClient = object : WebViewClient() {
        override fun shouldOverrideUrlLoading(
            view: WebView,
            request: WebResourceRequest,
        ): Boolean {
            if (!request.isForMainFrame) return false

            val target = request.url
            val staysInPrioSmith = target.scheme == prioSmithOrigin.scheme &&
                target.host == prioSmithOrigin.host &&
                target.port == prioSmithOrigin.port
            if (staysInPrioSmith) return false

            openExternalUrl(target)
            return true
        }
    }
    webView.loadUrl("https://app.priosmith.example/p/PROJECT_SLUG/board")
}

// Call this from the host screen's Back handler.
fun handlePrioSmithFeedbackBack(webView: WebView): Boolean {
    if (!webView.canGoBack()) return false
    webView.goBack()
    return true
}

Flutter web view

Toolchain-checked integration example — validate lifecycle in your app

Dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

// macOS: enable App Sandbox > Outgoing Connections (Client) on Runner.
class PrioSmithFeedbackView extends StatefulWidget {
  const PrioSmithFeedbackView({
    super.key,
    required this.openExternalUrl,
  });

  // Open external links, including sign-in, with a browser or Custom Tab.
  final void Function(Uri target) openExternalUrl;

  @override
  State<PrioSmithFeedbackView> createState() => _PrioSmithFeedbackViewState();
}

class _PrioSmithFeedbackViewState extends State<PrioSmithFeedbackView> {
  static final Uri _prioSmithOrigin = Uri.parse('https://app.priosmith.example');
  late final WebViewController _controller;
  bool _webViewCanGoBack = false;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onNavigationRequest: _handleNavigation,
          onPageFinished: _syncBackState,
          onUrlChange: (_) => _syncBackState(''),
        ),
      )
      ..loadRequest(Uri.parse('https://app.priosmith.example/p/PROJECT_SLUG/board'));
  }

  NavigationDecision _handleNavigation(NavigationRequest request) {
    if (!request.isMainFrame) return NavigationDecision.navigate;

    final target = Uri.tryParse(request.url);
    final staysInPrioSmith = target != null &&
        target.scheme == _prioSmithOrigin.scheme &&
        target.host == _prioSmithOrigin.host &&
        target.port == _prioSmithOrigin.port;
    if (staysInPrioSmith) return NavigationDecision.navigate;

    if (target != null) widget.openExternalUrl(target);
    return NavigationDecision.prevent;
  }

  Future<void> _syncBackState(String _) async {
    final canGoBack = await _controller.canGoBack();
    if (!mounted || canGoBack == _webViewCanGoBack) return;
    setState(() => _webViewCanGoBack = canGoBack);
  }

  Future<void> _handleBack(bool didPop) async {
    if (didPop || !_webViewCanGoBack) return;
    await _controller.goBack();
    await _syncBackState('');
  }

  @override
  Widget build(BuildContext context) {
    return PopScope<void>(
      canPop: !_webViewCanGoBack,
      onPopInvokedWithResult: (didPop, _) => _handleBack(didPop),
      child: SafeArea(child: WebViewWidget(controller: _controller)),
    );
  }
}

The client changes. The customer loop stays intact.

Keep collected requests connected to the public roadmap and the changelog customers can return to after a release ships.

Open the workshop