diff --git a/examples/expo-react-native/.gitignore b/examples/expo-react-native/.gitignore
new file mode 100644
index 00000000..05647d55
--- /dev/null
+++ b/examples/expo-react-native/.gitignore
@@ -0,0 +1,35 @@
+# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
+
+# dependencies
+node_modules/
+
+# Expo
+.expo/
+dist/
+web-build/
+
+# Native
+*.orig.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+
+# Metro
+.metro-health-check*
+
+# debug
+npm-debug.*
+yarn-debug.*
+yarn-error.*
+
+# macOS
+.DS_Store
+*.pem
+
+# local env files
+.env*.local
+
+# typescript
+*.tsbuildinfo
diff --git a/examples/expo-react-native/App.tsx b/examples/expo-react-native/App.tsx
new file mode 100644
index 00000000..595571b4
--- /dev/null
+++ b/examples/expo-react-native/App.tsx
@@ -0,0 +1,155 @@
+import {
+ StyleSheet,
+ View,
+ Modal,
+ Pressable,
+ Text,
+ ScrollView,
+} from "react-native";
+import React, { useState } from "react";
+
+import "./global.css";
+import { Creation } from "./src/creation";
+import { Cast } from "./src/cast";
+
+export default function App() {
+ return (
+
+
+
+
+ );
+}
+
+function RenderCreation() {
+ const [modalVisible, setModalVisible] = useState(false);
+ return (
+
+ {}}
+ >
+
+
+ setModalVisible(false)} />
+
+
+
+ setModalVisible(true)}
+ >
+ New Post
+
+
+ );
+}
+
+function RenderCasts() {
+ return (
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ centeredView: {
+ // flex: 1,
+ // flexGrow: 1,
+ // flexDirection: "column",
+ backgroundColor: "white",
+ alignItems: "center",
+ // alignSelf: "stretch",
+ },
+ modalView: {
+ marginHorizontal: 40,
+ backgroundColor: "white",
+ borderRadius: 20,
+ padding: 0,
+ width: "100%",
+ height: "100%",
+ alignItems: "center",
+ shadowColor: "#000",
+ shadowOffset: {
+ width: 0,
+ height: 2,
+ },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ elevation: 5,
+ },
+ button: {
+ borderRadius: 20,
+ padding: 10,
+ elevation: 2,
+ },
+ buttonOpen: {
+ backgroundColor: "#F194FF",
+ },
+ buttonClose: {
+ backgroundColor: "#2196F3",
+ },
+ textStyle: {
+ color: "white",
+ fontWeight: "bold",
+ textAlign: "center",
+ },
+ modalText: {
+ marginBottom: 15,
+ textAlign: "center",
+ },
+ container: {
+ flex: 1,
+ marginTop: 50,
+ flexGrow: 1,
+ flexDirection: "column",
+ backgroundColor: "white",
+ alignItems: "stretch",
+ alignSelf: "stretch",
+ // justifyContent: "stretch",
+ },
+});
diff --git a/examples/expo-react-native/app.json b/examples/expo-react-native/app.json
new file mode 100644
index 00000000..dc23eccc
--- /dev/null
+++ b/examples/expo-react-native/app.json
@@ -0,0 +1,30 @@
+{
+ "expo": {
+ "name": "expo-react-native",
+ "slug": "expo-react-native",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "userInterfaceStyle": "light",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#ffffff"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/examples/expo-react-native/assets/adaptive-icon.png b/examples/expo-react-native/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/examples/expo-react-native/assets/adaptive-icon.png differ
diff --git a/examples/expo-react-native/assets/favicon.png b/examples/expo-react-native/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/examples/expo-react-native/assets/favicon.png differ
diff --git a/examples/expo-react-native/assets/icon.png b/examples/expo-react-native/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/examples/expo-react-native/assets/icon.png differ
diff --git a/examples/expo-react-native/assets/splash.png b/examples/expo-react-native/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/examples/expo-react-native/assets/splash.png differ
diff --git a/examples/expo-react-native/babel.config.js b/examples/expo-react-native/babel.config.js
new file mode 100644
index 00000000..2ae1e1c5
--- /dev/null
+++ b/examples/expo-react-native/babel.config.js
@@ -0,0 +1,8 @@
+// babel.config.js
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ["babel-preset-expo"],
+ plugins: ["nativewind/babel"],
+ };
+};
diff --git a/examples/expo-react-native/global.css b/examples/expo-react-native/global.css
new file mode 100644
index 00000000..46dee059
--- /dev/null
+++ b/examples/expo-react-native/global.css
@@ -0,0 +1,4 @@
+// global.css
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/examples/expo-react-native/index.js b/examples/expo-react-native/index.js
new file mode 100644
index 00000000..828b3569
--- /dev/null
+++ b/examples/expo-react-native/index.js
@@ -0,0 +1,8 @@
+import { registerRootComponent } from "expo";
+
+import App from "./App";
+
+// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
+// It also ensures that whether you load the app in Expo Go or in a native build,
+// the environment is set up appropriately
+registerRootComponent(App);
diff --git a/examples/expo-react-native/metro.config.js b/examples/expo-react-native/metro.config.js
new file mode 100644
index 00000000..281bca24
--- /dev/null
+++ b/examples/expo-react-native/metro.config.js
@@ -0,0 +1,22 @@
+const { getDefaultConfig } = require("expo/metro-config");
+const path = require("path");
+
+// Find the project and workspace directories
+const projectRoot = __dirname;
+// This can be replaced with `find-yarn-workspace-root`
+const workspaceRoot = path.resolve(projectRoot, "../..");
+
+const config = getDefaultConfig(projectRoot, { isCSSEnabled: true });
+
+// 1. Watch all files within the monorepo
+config.watchFolders = [workspaceRoot];
+// 2. Let Metro know where to resolve packages and in what order
+config.resolver.nodeModulesPaths = [
+ path.resolve(projectRoot, "node_modules"),
+ path.resolve(workspaceRoot, "node_modules"),
+ path.resolve(workspaceRoot, "packages"),
+];
+// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
+config.resolver.disableHierarchicalLookup = true;
+
+module.exports = config;
diff --git a/examples/expo-react-native/package.json b/examples/expo-react-native/package.json
new file mode 100644
index 00000000..0c4df7c3
--- /dev/null
+++ b/examples/expo-react-native/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "expo-react-native",
+ "version": "1.0.0",
+ "main": "./index.js",
+ "private": true,
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web"
+ },
+ "dependencies": {
+ "expo": "~49.0.7",
+ "expo-status-bar": "~1.6.0",
+ "react": "18.2.0",
+ "expo-image": "^1.3.2",
+ "react-native": "0.72.3",
+ "nativewind": "^2.0.11",
+ "@packages/creation-react-native-headless": "*"
+ },
+ "devDependencies": {
+ "tailwindcss": "3.3.2",
+ "@babel/core": "^7.20.0",
+ "@types/node": "^20.4.10",
+ "@types/react": "^18.2.20",
+ "typescript": "^5.1.3"
+ },
+ "workspaces": [
+ "configs/*",
+ "packages/*",
+ "examples/*"
+ ]
+}
\ No newline at end of file
diff --git a/examples/expo-react-native/postcss.config.js b/examples/expo-react-native/postcss.config.js
new file mode 100644
index 00000000..45332d4c
--- /dev/null
+++ b/examples/expo-react-native/postcss.config.js
@@ -0,0 +1,6 @@
+// postcss.config.js
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ },
+};
diff --git a/examples/expo-react-native/src/bottom-bar.tsx b/examples/expo-react-native/src/bottom-bar.tsx
new file mode 100644
index 00000000..15ee4a1e
--- /dev/null
+++ b/examples/expo-react-native/src/bottom-bar.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Pressable, StyleSheet, Text, View } from "react-native";
+
+export function BottomBar(props: { onOpenMiniAppsModal: () => void }) {
+ return (
+
+ {
+ props.onOpenMiniAppsModal();
+ }}
+ >
+ +
+
+
+ );
+}
diff --git a/examples/expo-react-native/src/bottom-modal.tsx b/examples/expo-react-native/src/bottom-modal.tsx
new file mode 100644
index 00000000..ed2099cc
--- /dev/null
+++ b/examples/expo-react-native/src/bottom-modal.tsx
@@ -0,0 +1,52 @@
+import React from "react";
+
+import {
+ Modal,
+ SafeAreaView,
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ Text,
+} from "react-native";
+
+export const BottomModal = ({
+ open,
+ setModalOpen,
+ onRequestClose,
+ modalBackgroundStyle,
+ modalOptionsContainerStyle,
+ modalProps,
+ children,
+}: any) => {
+ if (!open) return null;
+ return (
+ setModalOpen?.(false)}
+ style={[
+ styles.modalContainer,
+ styles.modalOptionsContainer,
+ modalBackgroundStyle,
+ ]}
+ >
+
+ {children}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ modalContainer: {
+ flex: 1,
+ },
+ // modalBackgroundStyle: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
+ modalOptionsContainer: {
+ maxHeight: "50%",
+ borderColor: "#ccc",
+ borderWidth: 1,
+ borderBottomWidth: 0,
+ backgroundColor: "#fff",
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ },
+});
diff --git a/examples/expo-react-native/src/cast.tsx b/examples/expo-react-native/src/cast.tsx
new file mode 100644
index 00000000..ff5d0f61
--- /dev/null
+++ b/examples/expo-react-native/src/cast.tsx
@@ -0,0 +1,221 @@
+import { useRelativeDate } from "./relative-date";
+import React from "react";
+import Octicons from "@expo/vector-icons/Octicons";
+
+import {
+ StructuredCastImageUrl,
+ StructuredCastMention,
+ StructuredCastNewline,
+ StructuredCastPlaintext,
+ StructuredCastTextcut,
+ StructuredCastUnit,
+ StructuredCastUrl,
+ StructuredCastVideo,
+ convertCastPlainTextToStructured,
+} from "@packages/farcaster";
+import { Pressable, Text, View } from "react-native";
+import { Image } from "expo-image";
+
+export const structuredCastToReactDOMComponentsConfig: Record<
+ StructuredCastUnit["type"],
+ (structuredCast: any, i: number, options: {}) => React.ReactElement
+> = {
+ plaintext: (structuredCast: StructuredCastPlaintext, i: number, options) => (
+ {structuredCast.serializedContent}
+ ),
+ url: (structuredCast: StructuredCastUrl, i: number, options) => (
+
+ {structuredCast.serializedContent}
+
+ ),
+ videourl: (structuredCast: StructuredCastVideo, i: number, options) => (
+
+ {structuredCast.serializedContent}
+
+ ),
+ imageurl: (structuredCast: StructuredCastImageUrl, i: number, options) => (
+
+ {structuredCast.serializedContent}
+
+ ),
+ mention: (structuredCast: StructuredCastMention, i: number, options) => (
+
+ {structuredCast.serializedContent}
+
+ ),
+ textcut: (structuredCast: StructuredCastTextcut, i: number, options) => (
+
+ {structuredCast.serializedContent}
+
+ ),
+ newline: (_: StructuredCastNewline, i: number, options) => (
+
+ ),
+};
+
+export function convertStructuredCastToReactDOMComponents(
+ structuredCast: StructuredCastUnit[],
+ options: {}
+): (React.ReactElement | string)[] {
+ return structuredCast.map((structuredCastUnit, i) =>
+ structuredCastToReactDOMComponentsConfig[structuredCastUnit.type](
+ structuredCastUnit,
+ i,
+ options
+ )
+ );
+}
+
+type Embed =
+ | { alt: string; sourceUrl: string; type: "image"; url: string }
+ | {
+ type: "url";
+ openGraph: {
+ url: string;
+ image: string;
+ title: string;
+ domain: string;
+ };
+ };
+
+export function Cast(props: {
+ cast: {
+ avatar_url: string;
+ display_name: string;
+ username: string;
+ timestamp: string;
+ text: string;
+ embeds: Array