●MAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks AR/LiDAR scanning, Metal 3D, widgets, Live Activities, HealthKit, and more●FUNDING — Rork raised $2.8M from a16z, now drawing 743k+ monthly visits at an 85% growth rate●RN — Standard Rork generates iOS and Android apps together using React Native (Expo)●FOCUS — Rork focuses solely on native mobile apps, setting it apart from web-first Bolt and Lovable●PRICING — Free to start, paid plans from $25/mo, with Rork Max at $200/mo and two-click App Store publishing●MAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks AR/LiDAR scanning, Metal 3D, widgets, Live Activities, HealthKit, and more●FUNDING — Rork raised $2.8M from a16z, now drawing 743k+ monthly visits at an 85% growth rate●RN — Standard Rork generates iOS and Android apps together using React Native (Expo)●FOCUS — Rork focuses solely on native mobile apps, setting it apart from web-first Bolt and Lovable●PRICING — Free to start, paid plans from $25/mo, with Rork Max at $200/mo and two-click App Store publishing
Clipboard UX in Expo apps — copy and paste without flooding users with iOS's paste banner
When you wire up copy and paste with expo-clipboard, iOS's paste permission banner can fire constantly and quietly erode trust. Here's exactly when the banner appears, and how hasStringAsync lets you gate a Paste button without ever reading the contents.
When I added an invite-code redemption screen to one of my wallpaper apps, a small banner reading "Pasted from [other app]" appeared at the top of the screen every single time the screen opened. Testers kept asking me, "Is this actually safe?" The cause was obvious in hindsight: trying to be helpful, I was reading the clipboard the moment the screen mounted and auto-filling the code field. The instant you read, that banner fires.
Clipboard integration looks like a trivial feature — copy one line of text — but without understanding iOS's behavior, it quietly turns your app into a noisy one. Let me walk through how to use expo-clipboard so the paste banner only shows when it makes sense, while copy and paste still feel smooth.
Where the paste banner actually comes from
This trips people up: copying (writing) and pasting (reading) are treated completely differently.
Writing — Clipboard.setStringAsync() — produces no notification at all. It happens because the user pressed a copy button, so that's expected. The reading side is where the trouble is. Since iOS 16, reading content another app copied via Clipboard.getStringAsync() shows a "Pasted from [app]" banner. It exists to tell the user "this app just peeked at your clipboard," and you cannot turn it off.
This is why when you read matters so much. My mistake was reading before the user had done anything — on mount. A read that happens right after the user taps "Paste" produces a banner that matches the context, so it doesn't feel intrusive. A read on open, or on a polling interval, fires banners the user can't account for, and that breeds suspicion.
Action
API
iOS banner
Copy (write)
setStringAsync()
No
Read contents (paste)
getStringAsync()
Yes (iOS 16+)
Check presence only
hasStringAsync()
No
Use hasStringAsync to gate the button without reading
That last row is the key. hasStringAsync() returns a boolean for "is there a string on the clipboard" without ever reading the contents — so no banner. With it, you can disable the Paste button when the clipboard is empty and enable it only when something is there. You read the actual value only at the moment the user taps that button.
import { useEffect, useState } from "react";import * as Clipboard from "expo-clipboard";function RedeemCodeField({ onPaste }: { onPaste: (value: string) => void }) { const [canPaste, setCanPaste] = useState(false); // Checks presence only — never reads contents, so no banner useEffect(() => { let mounted = true; Clipboard.hasStringAsync().then((has) => { if (mounted) setCanPaste(has); }); return () => { mounted = false; }; }, []); // The real read happens only when the user taps the button const handlePaste = async () => { const text = await Clipboard.getStringAsync(); if (text) onPaste(text.trim()); }; return ( <PasteButton disabled={!canPaste} onPress={handlePaste} /> );}
Calling hasStringAsync() once on mount produces no banner. If you also want to re-check when the app returns to the foreground, call hasStringAsync() again from the AppStatechange event when state becomes active. You're still only checking presence, so it stays safe.
For something with a fixed shape like an invite code, don't drop the pasted string in verbatim. Run text.trim() to strip surrounding whitespace, then do a light validation against the expected format before applying it. That way the field doesn't break when a user accidentally copies extra characters along with the code.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦The exact conditions that trigger iOS's paste permission banner, and how to offer paste without multiplying it
✦An implementation pattern that uses hasStringAsync to enable a Paste button without reading the clipboard
✦Reliable copy feedback, plus how to clean up after putting sensitive data on the clipboard
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Writing doesn't show a banner, but that creates a different problem: it's hard to tell whether it succeeded. setStringAsync() returns true on success, so the dependable approach is to await it before showing any visual feedback.
import * as Clipboard from "expo-clipboard";import * as Haptics from "expo-haptics";async function copyInviteCode(code: string, showToast: (msg: string) => void) { const ok = await Clipboard.setStringAsync(code); if (ok) { await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); showToast("Code copied"); } else { showToast("Couldn't copy. Please try again."); }}
The point here is to not show the toast before awaiting the return value of setStringAsync(). Writing finishes almost instantly, but swallowing a failure leads to the worst outcome of all: the user thinks they copied something, and it was empty. Reserve haptic feedback for success only; a button that buzzes hard on every press feels cheap, so keep it subtle.
Temporarily switching the button label to "Copied" right after the copy and reverting it after a couple of seconds is another nice touch. Pairing it with a toast tends to be too much, so I prefer to pick one or the other.
Where iOS and Android differ
Since you're building cross-platform, it helps to know how the two systems diverge.
Aspect
iOS
Android
Notice on read
Banner (16+, can't disable)
Toast (12+, "pasted")
Banner on presence check
No
No
Sensitive flag
No expiring-paste in the standard API
Concept of sensitive content exists (13+)
The hasStringAsync() gating works on both. The wording and look of the read-time notice are up to the OS, so don't try to suppress it from your app. Sticking to the principle of "only read at a moment that matches a user action" is what works best in the end.
Cleaning up after sensitive data
When you let users copy short-lived secrets like a one-time code or token, plan for what happens after they're done. The clipboard is shared across apps, so leaving sensitive data sitting there keeps it readable from other apps too.
expo-clipboard has no standard API for copying with an expiration. So once the value has served its purpose, either call setStringAsync("") to explicitly clear it, or — better — don't route highly sensitive values through the clipboard at all and have users enter them directly in the app. Android 13 and later has a mechanism for distinguishing sensitive content, but expo-clipboard often can't specify it in detail, so the most reliable stance, in my view, is the blunt one: keep secrets off the clipboard wherever you can.
Values like invite codes, where a leak does little harm, are fine to pass through the clipboard. But deciding app-wide that anything tied to payments or login gets treated separately makes those calls much faster to make.
Pull it together into a reusable hook
Consolidating all of this into a single hook saves you from repeating the same care on every screen.
import { useCallback, useEffect, useState } from "react";import { AppState } from "react-native";import * as Clipboard from "expo-clipboard";export function useClipboard() { const [canPaste, setCanPaste] = useState(false); const refresh = useCallback(async () => { setCanPaste(await Clipboard.hasStringAsync()); }, []); useEffect(() => { refresh(); const sub = AppState.addEventListener("change", (state) => { if (state === "active") refresh(); }); return () => sub.remove(); }, [refresh]); const copy = useCallback(async (value: string) => { return Clipboard.setStringAsync(value); }, []); // Reading happens only from a user action on the caller side const paste = useCallback(async () => { const text = await Clipboard.getStringAsync(); return text.trim(); }, []); const clear = useCallback(async () => { await Clipboard.setStringAsync(""); setCanPaste(false); }, []); return { canPaste, copy, paste, clear, refresh };}
This hook exposes presence monitoring, writing, explicit reading, and cleanup as separate functions. As long as you honor one rule — never call paste() inside a useEffect, always call it from a button's onPress — the kind of needless banner I started with simply won't happen.
As a next step, grep your own app for where you call getStringAsync. If any of those calls run on mount or on focus, that's your banner source. Just moving that read behind an explicit user action will quietly improve the experience.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.