├── .dockerignore ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── api └── index.js ├── app-env.d.ts ├── app.json ├── app ├── (drawer) │ ├── (tabs) │ │ ├── _layout.tsx │ │ ├── index.tsx │ │ └── two.tsx │ ├── _layout.tsx │ └── index.tsx ├── +html.tsx ├── +not-found.tsx ├── _layout.tsx ├── api │ └── frames │ │ ├── examples │ │ ├── frameFourButton+api.ts │ │ ├── frameInput+api.ts │ │ ├── frameOneButton+api.ts │ │ ├── frameRedirect+api.ts │ │ ├── frameThreeButton+api.ts │ │ ├── frameTwoButton+api.ts │ │ └── responseFrame.ts │ │ └── helpers.ts └── modal.tsx ├── assets ├── adaptive-icon.png ├── examples │ ├── coderBlue.png │ ├── coderCafe.png │ ├── coderComic.png │ ├── coderComic2.png │ ├── coderCoworking.png │ ├── coderDesktop.png │ ├── coderDubai.png │ ├── coderFinance.png │ ├── coderFounder.png │ ├── coderHeadset.png │ ├── coderRed.png │ ├── coderSky.png │ ├── coderSquare.png │ └── coderWorking.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── components ├── framejs-expo │ ├── frame-debugger.tsx │ └── frame-render.tsx └── render-frame.tsx ├── docs └── README.md ├── frame ├── FrameContainer.tsx ├── FrameContainer.web.tsx ├── FrameDebugger.tsx ├── FrameDebugger.web.tsx └── utils │ ├── getFrame.ts │ ├── helpers.ts │ └── types.ts ├── global.css ├── index.ts ├── metro.config.js ├── nativewind-env.d.ts ├── netlify.toml ├── netlify └── functions │ └── server.js ├── package-lock.json ├── package.json ├── providers └── FarcasterKitProvider.tsx ├── server.js ├── tailwind.config.js ├── tsconfig.json ├── types.d.ts ├── utils └── firebase.ts └── vercel.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | .vercel/ 4 | dist/ 5 | npm-debug.* 6 | *.jks 7 | *.p8 8 | *.p12 9 | *.key 10 | *.mobileprovision 11 | *.orig.* 12 | web-build/ 13 | # expo router 14 | expo-env.d.ts 15 | 16 | # firebase/supabase 17 | .env 18 | 19 | # macOS 20 | .DS_Store 21 | 22 | # Temporary files created by Metro to check the health of the file watcher 23 | .metro-health-check* 24 | .vercel 25 | 26 | # Local Netlify folder 27 | .netlify 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | .vercel/ 4 | dist/ 5 | npm-debug.* 6 | *.jks 7 | *.p8 8 | *.p12 9 | *.key 10 | *.mobileprovision 11 | *.orig.* 12 | web-build/ 13 | # expo router 14 | expo-env.d.ts 15 | 16 | # firebase/supabase 17 | .env 18 | 19 | # macOS 20 | .DS_Store 21 | 22 | # Temporary files created by Metro to check the health of the file watcher 23 | .metro-health-check* 24 | .vercel 25 | 26 | # Local Netlify folder 27 | .netlify 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "bracketSameLine": true, 6 | "trailingComma": "es5" 7 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js image with LTS version based on Debian Bullseye slim 2 | FROM node:lts-bullseye-slim 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | RUN npm install 6 | COPY . . 7 | RUN npx expo export -p web 8 | EXPOSE 3000 9 | CMD ["node", "server.js"] 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expocaster 2 | 3 | >Expo mobile, web and API for creating Farcaster native applications. 4 | 5 | **Version: v0.0.0** 6 | 7 | The one stop repository for native mobile farcaster experiences offering the same to web and additional API building possibilities. 8 | 9 | That way with the same code written once you can 10 | 11 | - Run a native mobile app 12 | - Run a responsive web app 13 | - Farcaster Frames Backend and general API/backend 14 | 15 | Write once, enjoy everywhere. 16 | 17 | [Contribute](#contribute) 18 | 19 | ## Getting Started 20 | 21 | to test follow instructions for full expo or use [Docker for Web + API routes](#dockerfile) 22 | 1) Clone the repository 23 | 1) ```npm install``` 24 | 1) ```npm cache clean --force``` 25 | 1) ```npm expo start``` 26 | 27 | You'll now be greated by Expo where you can [check their docs](https://docs.expo.dev/get-started/expo-go/) 28 | 29 | [Create-Expo-Stack](createexpostack.com) was used to jumpstart this repo in one command ```npx create-expo-stack@latest expocaster --expo-router --drawer+tabs --nativewind --firebase``` 30 | 31 | ## built using Farcaster Ecosystem awesomness 32 | 33 | - [Farcasterkit - React Hooks](https://www.farcasterkit.com/) 34 | 35 | ``` 36 | npm install farcasterkit 37 | ``` 38 | 39 | - [Neynar Types and optional API integration](neynar.com) 40 | 41 | - [Litecast - Inspired Frame Rendering](https://github.com/dylsteck/litecast) 42 | 43 | ## also built on top of Typescript awesomeness 44 | 45 | - [Create Expo Stack - spin up an expo mobile, web, api stack the way it should be in 10s](https://createexpostack.com/) 46 | 47 | ## Contribute 48 | 49 | The stack used has NativeWind for styling so you can use the typical ```className=""``` syntax your might now from web with Tailwind. 50 | 51 | otherwise it's straight Typescript and Expo. 52 | 53 | ### To contribute changes/improvements 54 | 55 | 1) fork the repository 56 | 2) make your changes 57 | 3) submit a PR 58 | 59 | ### To contribute feedback and bug reports 60 | 61 | 1) create an Issue 62 | 63 | # Deployment of Frames and Web 64 | 65 | >Official Expo [Deployment Documentation here](https://vercel.com/docs/cli/deploying-from-cli#deploying-from-local-build-prebuilt) to be used as most up to date document and reference checked if the below doesn't work! 66 | 67 | ## Dockerfile 68 | 69 | >IMPORTANT: use the .dockerignore to control what is included in the Docker Image! .env is excluded and env variables shall be used. 70 | 71 | to build 72 | ``` 73 | docker build -t expocaster . 74 | ``` 75 | 76 | to run 77 | ``` 78 | docker run -d -p 3000:3000 expocaster 79 | ``` 80 | 81 | We are using [NodeJS](#nodejs-with-express) as deployment in the Dockerfile trying to build the [smallest Dockerfile](https://snyk.io/blog/choosing-the-best-node-js-docker-image/) we can (without using distroless). 82 | 83 | To install Docker on Ubuntu go [here](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) also make sure to [use docker group](https://docs.docker.com/engine/install/linux-postinstall/) instead of sudo if possible :) 84 | 85 | Now by building the docker container you can deploy anywhere that supports docker including Kubernetes, DigitalOcean Droplets, Akash.network and the likes. 86 | 87 | ## Kubernetes (Helm Chart) 88 | 89 | A Helmchart deployment for the sample, where you can simply change the Dockerfile of your choosing and get up and running will follow soon. 90 | 91 | ## NodeJS with Express 92 | 93 | > Expo's [Official Documentation can be found here and should be referenced if the below doesn't work](https://docs.expo.dev/router/reference/api-routes/#express) 94 | 95 | the one command build expo web for prod and use express routing 96 | 97 | ``` 98 | npx expo export -p web && cp server.js dist/ && cp package.json dist/ 99 | ``` 100 | 101 | now you can copy the contents of `dist/` to your nodeJS server root dir and serve with 102 | ``` 103 | node server.js 104 | ``` 105 | 106 | ## Vercel 107 | 108 | >NOTE: this hasn't been made work, but is what documentation states! If you make it work please open a PR! 109 | 110 | If your remote build doesn't work you may try [prebuilding and then uploading](https://vercel.com/docs/cli/deploying-from-cli#deploying-from-local-build-prebuilt) like so: 111 | 112 | this assumes you have vercel installed (```npx vercel```) and configured 113 | 114 | ``` 115 | npx vercel build && npx vercel deploy --prebuilt 116 | ``` 117 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const { createRequestHandler } = require('@expo/server/adapter/vercel'); 2 | 3 | module.exports = createRequestHandler({ 4 | build: require('path').join(__dirname, '../dist/server'), 5 | mode: process.env.NODE_ENV, 6 | }); 7 | -------------------------------------------------------------------------------- /app-env.d.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /// 3 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "expocaster", 4 | "slug": "expocaster", 5 | "version": "1.0.0", 6 | 7 | "scheme": "expocaster", 8 | "web": { 9 | "bundler": "metro", 10 | "output": "server", 11 | "favicon": "./assets/favicon.png" 12 | }, 13 | "plugins": ["expo-router"], 14 | "experiments": { 15 | "typedRoutes": true, 16 | 17 | "tsconfigPaths": true 18 | }, 19 | 20 | "orientation": "portrait", 21 | "icon": "./assets/icon.png", 22 | "userInterfaceStyle": "light", 23 | "splash": { 24 | "image": "./assets/splash.png", 25 | "resizeMode": "contain", 26 | "backgroundColor": "#ffffff" 27 | }, 28 | "assetBundlePatterns": ["**/*", "assets/**/*"], 29 | "ios": { 30 | "supportsTablet": true 31 | }, 32 | "android": { 33 | "adaptiveIcon": { 34 | "foregroundImage": "./assets/adaptive-icon.png", 35 | "backgroundColor": "#ffffff" 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/(drawer)/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import FontAwesome from '@expo/vector-icons/FontAwesome'; 2 | import { Tabs } from 'expo-router'; 3 | import { StyleSheet } from 'react-native'; 4 | 5 | function TabBarIcon(props: { 6 | name: React.ComponentProps['name']; 7 | color: string; 8 | }) { 9 | return ; 10 | } 11 | 12 | export default function TabLayout() { 13 | return ( 14 | 20 | , 25 | }} 26 | /> 27 | , 32 | }} 33 | /> 34 | 35 | ); 36 | } 37 | 38 | const styles = StyleSheet.create({ 39 | tabBarIcon: { 40 | marginBottom: -3, 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /app/(drawer)/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollView, StyleSheet, useWindowDimensions } from 'react-native'; 3 | 4 | import RenderFrame from '~/components/render-frame'; 5 | 6 | const FRAME_PUBLIC_URL = __DEV__ ? 'http://localhost:8081' : 'https://expocaster.netlify.app/'; 7 | const LOCAL_FRAME_URL = FRAME_PUBLIC_URL + '/api/frames/examples/sampleInput'; 8 | const Frames = [ 9 | { 10 | frameUrl: LOCAL_FRAME_URL, 11 | title: 'Local Frame', 12 | }, 13 | { 14 | frameUrl: 'https://yoink.terminally.online', 15 | title: 'Post Redirect', 16 | }, 17 | { 18 | frameUrl: 'https://farm.cropxyz.com', 19 | title: '1 Button', 20 | }, 21 | { 22 | frameUrl: 'https://alliance-frame.vercel.app/', 23 | title: 'With Input', 24 | }, 25 | 26 | { 27 | frameUrl: 'https://frames.neynar.com/f/6f89a5ac/505cd276', 28 | title: 'Aspect Ratio Frame', 29 | }, 30 | 31 | { 32 | frameUrl: 'https://sol-drop-frame.vercel.app/', 33 | title: 'With Input -Personal Frame', 34 | }, 35 | ]; 36 | 37 | const TabOneScreen = () => { 38 | const { width } = useWindowDimensions(); 39 | const isMobile = width < 768; 40 | return ( 41 | 42 | {Frames.map((frame, index) => { 43 | return ; 44 | })} 45 | {/* item.frameUrl} renderItem={renderItem} /> */} 46 | 47 | ); 48 | }; 49 | 50 | export default TabOneScreen; 51 | 52 | const Styles = StyleSheet.create({ 53 | container: { 54 | alignSelf: 'center', 55 | alignItems: 'center', 56 | justifyContent: 'center', 57 | padding: 8, 58 | }, 59 | separator: { 60 | marginVertical: 30, 61 | height: 1, 62 | width: '80%', 63 | }, 64 | title: { 65 | fontSize: 20, 66 | fontWeight: 'bold', 67 | }, 68 | }); 69 | const styles = { 70 | container: `items-center flex-1 justify-center`, 71 | separator: `h-[1px] my-7 w-4/5 bg-gray-200`, 72 | title: `text-xl font-bold`, 73 | }; 74 | -------------------------------------------------------------------------------- /app/(drawer)/(tabs)/two.tsx: -------------------------------------------------------------------------------- 1 | import { Text, View, StyleSheet } from 'react-native'; 2 | import FrameDebugger from '~/frame/FrameDebugger'; 3 | 4 | export default function TabTwoScreen() { 5 | return ; 6 | } 7 | 8 | 9 | // const styles = { 10 | // container: `items-center flex-1 justify-center`, 11 | // separator: `h-[1px] my-7 w-4/5 bg-gray-200`, 12 | // title: `text-xl font-bold`, 13 | // }; 14 | -------------------------------------------------------------------------------- /app/(drawer)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesome, Ionicons, MaterialIcons } from '@expo/vector-icons'; 2 | import { Link } from 'expo-router'; 3 | import { Drawer } from 'expo-router/drawer'; 4 | import { Pressable, StyleSheet, Platform, useWindowDimensions } from 'react-native'; 5 | 6 | const DrawerLayout = () => { 7 | const { width } = useWindowDimensions(); 8 | const isMobile = width < 768; 9 | return ( 10 | 18 | ( 24 | 25 | ), 26 | }} 27 | /> 28 | ( 34 | 35 | ), 36 | headerRight: () => ( 37 | 38 | 39 | {({ pressed }) => ( 40 | 46 | )} 47 | 48 | 49 | ), 50 | }} 51 | /> 52 | 53 | ); 54 | }; 55 | 56 | const styles = StyleSheet.create({ 57 | headerRight: { 58 | marginRight: 15, 59 | }, 60 | }); 61 | 62 | export default DrawerLayout; 63 | -------------------------------------------------------------------------------- /app/(drawer)/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View,StyleSheet,Text}from "react-native" 3 | const Page = () => { 4 | return ( 5 | 6 | Hello 7 | 8 | ); 9 | }; 10 | 11 | export default Page; 12 | 13 | const Styles=StyleSheet.create({ 14 | container:{ 15 | flex:1, 16 | justifyContent:"center", 17 | alignItems:"center" 18 | } 19 | }) 20 | const styles = { 21 | container: `items-center flex-1 justify-center`, 22 | separator: `h-[1px] my-7 w-4/5 bg-gray-200`, 23 | title: `text-xl font-bold`, 24 | }; 25 | -------------------------------------------------------------------------------- /app/+html.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollViewStyleReset } from 'expo-router/html'; 2 | 3 | // This file is web-only and used to configure the root HTML for every 4 | // web page during static rendering. 5 | // The contents of this function only run in Node.js environments and 6 | // do not have access to the DOM or browser APIs. 7 | export default function Root({ children }: { children: React.ReactNode }) { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | {/* 15 | This viewport disables scaling which makes the mobile website act more like a native app. 16 | However this does reduce built-in accessibility. If you want to enable scaling, use this instead: 17 | 18 | */} 19 | 23 | {/* 24 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 25 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 26 | */} 27 | 28 | 29 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 30 |