Open your board from a mobile app
Give feedback opens your hosted board first, in the platform browser surface. No native form, WebView, indifeed SDK, or server identity integration is needed.
Validate these examples in your app on simulators/emulators and physical devices before release. Browser viewport emulation is not device acceptance; these examples are not a claim of completed native-device testing.
Configure your board URL
Replace https://acme.indifeed.app with your own configured HTTPS board base URL. Open the root, not /new or a request deep link. Pass no profile, token, API key, or query parameters. The hosted board handles browsing and participation.
The examples reject invalid configuration without crashing. Board loading, connectivity, and submission errors are displayed by the hosted page; a failed submission should leave the open form available to retry. There is no offline queue and no draft persistence after dismiss: treat unsent board text as lost when closing the browser. The host app must retain its own in-progress work independently.
iOS — UIKit and SafariServices
Add this controller to your existing UIKit app and show it through your normal navigation (for example navigationController?.pushViewController(FeedbackViewController(), animated: true)). It creates a labelled button and presents SFSafariViewController modally after the tap, using the default presentation style. Done returns to your app. No third-party dependency is required. [1]
import UIKit
import SafariServices
final class FeedbackViewController: UIViewController {
private let boardBaseURL = "https://acme.indifeed.app"
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let button = UIButton(type: .system)
button.setTitle("Give feedback", for: .normal)
button.accessibilityLabel = "Give feedback"
button.titleLabel?.font = .preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(openFeedback), for: .touchUpInside)
view.addSubview(button)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
button.heightAnchor.constraint(greaterThanOrEqualToConstant: 48),
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 48),
button.leadingAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
button.trailingAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16)
])
}
@objc private func openFeedback() {
guard presentedViewController == nil, view.window != nil else { return }
guard let parts = URLComponents(string: boardBaseURL),
parts.scheme == "https",
let host = parts.host, !host.isEmpty,
parts.user == nil, parts.password == nil,
parts.query == nil, parts.fragment == nil,
parts.path.isEmpty || parts.path == "/",
let url = parts.url else {
let alert = UIAlertController(title: "Feedback unavailable",
message: "The feedback board URL is not configured correctly. Please contact support.",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
return
}
present(SFSafariViewController(url: url), animated: true)
}
}
Keep the browser visible; do not embed it as a child controller, obscure it, or attempt to inspect its website data. SafariServices handles page-load errors inside the browser; presentation itself has no throwing launch API. [1]
Android — Kotlin Activity and Custom Tabs
In your existing Android app module's Kotlin Gradle dependency block, add the released stable version below (1.8.0, released March 6, 2024; not a claim that it is the latest). [3] Register FeedbackActivity in your existing manifest and open it with startActivity(Intent(this, FeedbackActivity::class.java)) from your app's navigation.
dependencies {
implementation("androidx.browser:browser:1.8.0")
}
A Custom Tab uses a supporting browser, not necessarily Chrome. An unpinned Custom Tabs intent can open the external browser when Custom Tabs are unsupported. [2] The example also retries with a plain HTTPS browser intent if launch fails, and shows an error if no handler can open it. No package discovery or package-visibility manifest queries are needed.
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Button
import android.widget.FrameLayout
import androidx.browser.customtabs.CustomTabsIntent
class FeedbackActivity : Activity() {
private val boardBaseUrl = "https://acme.indifeed.app"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val button = Button(this).apply {
text = "Give feedback"
contentDescription = "Give feedback"
minHeight = (48 * resources.displayMetrics.density).toInt()
setOnClickListener { openFeedback() }
}
val container = FrameLayout(this).apply {
fitsSystemWindows = true
addView(button, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
android.view.Gravity.CENTER
))
}
setContentView(container)
}
private fun openFeedback() {
val uri = Uri.parse(boardBaseUrl)
if (uri.scheme != "https" || uri.host.isNullOrBlank() ||
uri.userInfo != null || uri.query != null || uri.fragment != null ||
(!uri.path.isNullOrEmpty() && uri.path != "/")) {
showError("The feedback board URL is not configured correctly. Please contact support.")
return
}
try {
CustomTabsIntent.Builder().setShowTitle(true).build().launchUrl(this, uri)
return
} catch (_: ActivityNotFoundException) {
// Retry with an ordinary browser intent below.
} catch (_: SecurityException) {
// A browser may be disabled or blocked by device policy.
}
try {
startActivity(Intent(Intent.ACTION_VIEW, uri).addCategory(Intent.CATEGORY_BROWSABLE))
} catch (_: ActivityNotFoundException) {
showError("No browser is available. Install or enable a browser and try again.")
} catch (_: SecurityException) {
showError("The browser could not be opened. Check your device restrictions and try again.")
}
}
private fun showError(message: String) {
AlertDialog.Builder(this)
.setTitle("Feedback unavailable")
.setMessage(message)
.setPositiveButton("OK", null)
.show()
}
}
Back or the browser close control returns to your app; the external-browser fallback may require the system Back control. There is no app↔web messaging or dismissal callback used to track board activity.
Guest sessions and board restrictions
On a guest-enabled active board, visitors can browse, create requests, vote, comment, and watch. Guest name and email are optional; they are entered in the hosted board, not collected by the native button. A generated name is used when a guest supplies no name.
Guest identity is browser-local convenience state, not authentication. Clearing cookies, private browsing, or changing devices can lose it. Native login does not identify the browser user. iOS does not expose the browser view's website data to your app. [1] Android Custom Tabs use the selected browser's cookie jar and permissions. [2] Test reopening without promising persistence or a shared native-app session.
When guest participation is disabled, guests may browse but cannot submit, vote, comment, or watch; the board shows its configured known-user message. Read-only boards prevent participation, and offline boards show the closed-board experience. Do not bypass these restrictions or invent your own policy messages in the native app.
This integration implements only opening the hosted board. Signed native identity handoff and native request forms are not included.
What gets sent
The button passes only the configured board URL to the browser. Loading the board sends normal HTTPS page/API requests, browser/session information, and existing hosted SPA analytics as usual. Participation sends the user's feedback text, actions such as votes/comments/watch, and any optional name/email entered on the board. These examples add no extra collected diagnostics, device identifiers, native login profile, or app-side analytics, and do not track browser interactions.
Before shipping
On physical iOS and Android devices, verify launch, dismiss/back, reopening, guest create/vote/comment/watch, guest-disabled/read-only/offline boards, a failed submission, keyboard visibility, portrait/landscape safe areas, and large text. Test VoiceOver and TalkBack labels and hosted-board controls. On Android also test a browser without Custom Tabs and a device with no available browser. Do not treat a desktop browser's mobile viewport as this acceptance test.