Initial commit: code as received (Create/Anything export)
Insole-production time tracker exported from the Create/Anything AI platform. Baseline snapshot before any reverse-engineering or cleanup. - apps/mobile: Expo Router app (iOS/Android/web), the only workspace - publisher/: standalone OpenNext/AWS deploy tooling for the web side - Backend (/api/tasks, /api/logs + DB) lives remotely, not in this repo
This commit is contained in:
105
apps/mobile/src/__create/ErrorBoundary.tsx
Normal file
105
apps/mobile/src/__create/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { Component, type ReactNode } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
function postErrorToParent(error: Error) {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'sandbox:error:detected',
|
||||
error: {
|
||||
message: error.message,
|
||||
name: error.name || 'Error',
|
||||
stack: error.stack || '',
|
||||
},
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function postErrorResolvedToParent() {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'sandbox:error:resolved' }, '*');
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = { hasError: false, error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
postErrorToParent(error);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Something went wrong</Text>
|
||||
<Text style={styles.message}>
|
||||
{this.state.error?.message ?? 'An unexpected error occurred'}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={() => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
postErrorResolvedToParent();
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>Try again</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 24,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#18191B',
|
||||
marginBottom: 8,
|
||||
},
|
||||
message: {
|
||||
fontSize: 14,
|
||||
color: '#959697',
|
||||
textAlign: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#18191B',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
84
apps/mobile/src/__create/analytics.ts
Normal file
84
apps/mobile/src/__create/analytics.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { usePathname } from "expo-router";
|
||||
import { useEffect } from "react";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
const VISITOR_ID_KEY = "anything_analytics_visitor_id";
|
||||
|
||||
// Mirror the gating used by Sentry / the TestFlight logger: only emit from
|
||||
// real (production) builds, never from the in-builder dev runtime.
|
||||
function isActive(): boolean {
|
||||
return !__DEV__ && process.env.EXPO_PUBLIC_CREATE_ENV !== "DEVELOPMENT";
|
||||
}
|
||||
|
||||
function generateVisitorId(): string {
|
||||
const rand = () => Math.random().toString(36).slice(2);
|
||||
return `${rand()}${rand()}`.slice(0, 32);
|
||||
}
|
||||
|
||||
let visitorIdPromise: Promise<string> | null = null;
|
||||
|
||||
// Stable, anonymous, per-install id. Not a secret, so AsyncStorage (not the
|
||||
// keychain) is the right home. Generated once and reused for the install.
|
||||
function getVisitorId(): Promise<string> {
|
||||
if (!visitorIdPromise) {
|
||||
visitorIdPromise = (async () => {
|
||||
try {
|
||||
const existing = await AsyncStorage.getItem(VISITOR_ID_KEY);
|
||||
if (existing) return existing;
|
||||
const created = generateVisitorId();
|
||||
await AsyncStorage.setItem(VISITOR_ID_KEY, created);
|
||||
return created;
|
||||
} catch {
|
||||
// If persistence fails, fall back to a session-scoped id so the
|
||||
// current run still attributes its views to one visitor.
|
||||
return generateVisitorId();
|
||||
}
|
||||
})();
|
||||
}
|
||||
return visitorIdPromise;
|
||||
}
|
||||
|
||||
// Records one screen view per route change. The endpoint enforces the global
|
||||
// flag and the project's analytics opt-in, dropping events (204) when off, so
|
||||
// this always fires and the server decides whether to keep it.
|
||||
export function ScreenViewTracker() {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive()) return;
|
||||
|
||||
const endpoint = process.env.EXPO_PUBLIC_ANALYTICS_ENDPOINT;
|
||||
const host = process.env.EXPO_PUBLIC_HOST;
|
||||
const projectGroupId = process.env.EXPO_PUBLIC_PROJECT_GROUP_ID;
|
||||
if (!endpoint || !host || !projectGroupId || !pathname) return;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const visitorId = await getVisitorId();
|
||||
if (cancelled) return;
|
||||
await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
d: host,
|
||||
p: pathname,
|
||||
pgid: projectGroupId,
|
||||
vid: visitorId,
|
||||
os: Platform.OS,
|
||||
dt: "mobile",
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// Analytics must never crash or block the host app.
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
19
apps/mobile/src/__create/anything-menu.ios.tsx
Normal file
19
apps/mobile/src/__create/anything-menu.ios.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import LauncherMenuContainer from '@anythingai/app/screens/launcher-menu';
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
const isExpoGo = globalThis.expo?.modules?.ExpoGo;
|
||||
|
||||
export default () => {
|
||||
if (isExpoGo) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={{ ...StyleSheet.absoluteFillObject, zIndex: 9999 }}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<LauncherMenuContainer />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
586
apps/mobile/src/__create/anything-menu.tsx
Normal file
586
apps/mobile/src/__create/anything-menu.tsx
Normal file
@@ -0,0 +1,586 @@
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useMemo, memo, useRef, useReducer } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
PanResponder,
|
||||
Platform,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
interpolate,
|
||||
withTiming,
|
||||
Easing,
|
||||
} from "react-native-reanimated";
|
||||
import {
|
||||
SafeAreaProvider,
|
||||
useSafeAreaInsets,
|
||||
} from "react-native-safe-area-context";
|
||||
import Svg, {
|
||||
Path,
|
||||
Rect,
|
||||
Mask,
|
||||
Circle,
|
||||
G,
|
||||
Defs,
|
||||
ClipPath,
|
||||
Line,
|
||||
} from "react-native-svg";
|
||||
import { NativeModule, requireNativeModule } from "expo-modules-core";
|
||||
import { MotiView } from "moti";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { WebView } from "react-native-webview";
|
||||
|
||||
declare class AnythingLauncherModule extends NativeModule {
|
||||
open(url: string): Promise<void>;
|
||||
reset(): Promise<void>;
|
||||
reload(): Promise<void>;
|
||||
isWeb(): Promise<boolean>;
|
||||
}
|
||||
|
||||
const TINT_DURATION_MS = 3000;
|
||||
const CIRCLE_DIAMETER = 80;
|
||||
const GAP = 16;
|
||||
const ICON_SIZE = 18;
|
||||
|
||||
const getWebAppUrl = () => {
|
||||
return process.env.EXPO_PUBLIC_APP_URL ?? "";
|
||||
};
|
||||
|
||||
const isAnythingApp =
|
||||
Platform.OS !== "web" &&
|
||||
process.env.EXPO_PUBLIC_IS_ANYTHING_APP === JSON.stringify(true);
|
||||
|
||||
const AnythingLauncher = isAnythingApp
|
||||
? requireNativeModule<AnythingLauncherModule>("AnythingLauncherModule")
|
||||
: null;
|
||||
|
||||
const RefreshIcon = memo(() => {
|
||||
return (
|
||||
<Svg width={ICON_SIZE} height={ICON_SIZE} viewBox="0 0 18 18" fill="none">
|
||||
<Path
|
||||
d="M1.5 7.5s1.504-2.049 2.725-3.271a6.75 6.75 0 11-1.712 6.646M1.5 7.5V3m0 4.5H6"
|
||||
stroke="#7E7F80"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
});
|
||||
|
||||
const CloseIcon = memo(() => {
|
||||
return (
|
||||
<Svg width={ICON_SIZE} height={ICON_SIZE} viewBox="0 0 18 18" fill="none">
|
||||
<Path
|
||||
d="M2.25 15.75l13.5-13.5M15.75 15.75L2.25 2.25"
|
||||
stroke="#7E7F80"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
});
|
||||
|
||||
const MobileViewIcon = memo(({ color }: { color: string }) => {
|
||||
return (
|
||||
<Svg width={ICON_SIZE} height={ICON_SIZE} viewBox="0 0 18 18" fill="none">
|
||||
<Path
|
||||
d="M11.8125 1.5H6.1875C5.15197 1.5 4.3125 2.33947 4.3125 3.375V14.625C4.3125 15.6605 5.15197 16.5 6.1875 16.5H11.8125C12.848 16.5 13.6875 15.6605 13.6875 14.625V3.375C13.6875 2.33947 12.848 1.5 11.8125 1.5Z"
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Line
|
||||
x1={7.89575}
|
||||
y1={13.3832}
|
||||
x2={10.104}
|
||||
y2={13.3832}
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
});
|
||||
|
||||
const WebViewIcon = memo(({ color }: { color: string }) => {
|
||||
return (
|
||||
<Svg width={ICON_SIZE} height={ICON_SIZE} viewBox="0 0 18 18" fill="none">
|
||||
<G clipPath="url(#clip0_340_2754)">
|
||||
<Path
|
||||
d="M15 1.5H3C2.17157 1.5 1.5 2.17157 1.5 3V12C1.5 12.8284 2.17157 13.5 3 13.5H15C15.8284 13.5 16.5 12.8284 16.5 12V3C16.5 2.17157 15.8284 1.5 15 1.5Z"
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Path
|
||||
d="M9 13.5V16.5"
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Path
|
||||
d="M6 16.5H12"
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</G>
|
||||
<Defs>
|
||||
<ClipPath id="clip0_340_2754">
|
||||
<Rect width={18} height={18} fill="white" />
|
||||
</ClipPath>
|
||||
</Defs>
|
||||
</Svg>
|
||||
);
|
||||
});
|
||||
|
||||
const ActiveDot = memo(() => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#000",
|
||||
borderRadius: 50,
|
||||
width: 4,
|
||||
height: 4,
|
||||
position: "absolute",
|
||||
bottom: -8,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const InstructionsOverlay = memo(
|
||||
({
|
||||
showTint,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
showTint: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => {
|
||||
const r = CIRCLE_DIAMETER / 2;
|
||||
const totalWidth = CIRCLE_DIAMETER * 2 + GAP;
|
||||
const left = (width - totalWidth) / 2;
|
||||
const cx1 = left + r;
|
||||
const cx2 = cx1 + CIRCLE_DIAMETER + GAP;
|
||||
const cy = height / 2 + 64;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MotiView
|
||||
from={{ opacity: 0 }}
|
||||
animate={{ opacity: showTint ? 1 : 0 }}
|
||||
transition={{ type: "timing", duration: 350 }}
|
||||
style={menuStyles.holdTwoFingersTextContainer}
|
||||
>
|
||||
<Text style={menuStyles.holdTwoFingersText}>
|
||||
Hold with 2 fingers for menu
|
||||
</Text>
|
||||
</MotiView>
|
||||
<MotiView
|
||||
from={{ opacity: 0 }}
|
||||
animate={{ opacity: showTint ? 1 : 0 }}
|
||||
transition={{ type: "timing", duration: 350 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
>
|
||||
<Svg width={width} height={height} style={StyleSheet.absoluteFill}>
|
||||
<Mask id="holes">
|
||||
<Rect x="0" y="0" width={width} height={height} fill="white" />
|
||||
<Circle cx={cx1} cy={cy} r={r} fill="black" />
|
||||
<Circle cx={cx2} cy={cy} r={r} fill="black" />
|
||||
</Mask>
|
||||
|
||||
<Rect
|
||||
x="0"
|
||||
y="0"
|
||||
width={width}
|
||||
height={height}
|
||||
fill="black"
|
||||
opacity={0.8}
|
||||
mask="url(#holes)"
|
||||
/>
|
||||
</Svg>
|
||||
</MotiView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type State = {
|
||||
isLoading: boolean;
|
||||
showTint: boolean;
|
||||
showWebView: boolean;
|
||||
}
|
||||
|
||||
type Action = { type: 'INITIALIZE', payload: { showWebView: boolean, showTint: boolean } } | { type: 'TOGGLE_WEB_VIEW' } | { type: 'HIDE_TINT' }
|
||||
|
||||
const initialState: State = { isLoading: true, showTint: false, showWebView: false };
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'INITIALIZE':
|
||||
return { ...state, ...action.payload, isLoading: false };
|
||||
case 'TOGGLE_WEB_VIEW':
|
||||
return { ...state, showWebView: !state.showWebView };
|
||||
case 'HIDE_TINT':
|
||||
return { ...state, showTint: false };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const AnythingMenu = isAnythingApp
|
||||
? ({ children }: { children: React.ReactNode }) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { width, height } = useWindowDimensions();
|
||||
|
||||
useEffect(() => {
|
||||
if (!AnythingLauncher) {
|
||||
throw new Error("AnythingLauncher is not available");
|
||||
}
|
||||
|
||||
if (state.isLoading) {
|
||||
Promise.all([
|
||||
AnythingLauncher.isWeb(),
|
||||
AsyncStorage.getItem("hasSeenOnboarding"),
|
||||
]).then(([isWeb, hasSeenOnboarding]) => {
|
||||
dispatch({ type: 'INITIALIZE', payload: { showWebView: Boolean(isWeb), showTint: hasSeenOnboarding !== 'true' } });
|
||||
}).catch(() => {
|
||||
dispatch({ type: 'INITIALIZE', payload: { showWebView: false, showTint: false } });
|
||||
});
|
||||
}
|
||||
}, [state.isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state.isLoading && state.showTint) {
|
||||
const timeout = setTimeout(() => {
|
||||
void AsyncStorage.setItem("hasSeenOnboarding", "true");
|
||||
dispatch({ type: 'HIDE_TINT' });
|
||||
}, TINT_DURATION_MS);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
|
||||
}, [state.isLoading, state.showTint])
|
||||
|
||||
const menuProgress = useSharedValue(0);
|
||||
|
||||
const hideMenuOffset = -(44 + 36 + insets.top + 10);
|
||||
|
||||
const exitApp = useCallback(() => {
|
||||
void AnythingLauncher?.reset();
|
||||
}, []);
|
||||
|
||||
const reloadApp = useCallback(() => {
|
||||
void AnythingLauncher?.reload();
|
||||
}, []);
|
||||
|
||||
const toggleWebView = useCallback(() => {
|
||||
dispatch({ type: 'TOGGLE_WEB_VIEW' });
|
||||
}, []);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
const scale = interpolate(menuProgress.value, [0, 1], [1, 0.9]);
|
||||
const shadowOpacity = interpolate(menuProgress.value, [0, 1], [0, 0.4]);
|
||||
const elevation = interpolate(menuProgress.value, [0, 1], [0, 8]);
|
||||
|
||||
return {
|
||||
transform: [{ scale }],
|
||||
shadowOpacity,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowRadius: 32,
|
||||
elevation,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const menuAnimatedStyle = useAnimatedStyle(() => {
|
||||
const translateY = interpolate(
|
||||
menuProgress.value,
|
||||
[0, 1],
|
||||
[hideMenuOffset, 0]
|
||||
);
|
||||
|
||||
return {
|
||||
transform: [{ translateY }],
|
||||
};
|
||||
}, [hideMenuOffset]);
|
||||
|
||||
const appPointerEvents = useAnimatedStyle(() => {
|
||||
return {
|
||||
pointerEvents: menuProgress.value === 1 ? "box-only" : "auto",
|
||||
};
|
||||
}, [menuProgress]);
|
||||
|
||||
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const panResponder = useMemo(
|
||||
() =>
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: (evt, gestureState) => {
|
||||
if (menuProgress.value === 1) {
|
||||
menuProgress.value = withTiming(0, {
|
||||
duration: 300,
|
||||
easing: Easing.ease,
|
||||
});
|
||||
if (longPressTimer.current) {
|
||||
clearTimeout(longPressTimer.current);
|
||||
longPressTimer.current = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (gestureState.numberActiveTouches === 2) {
|
||||
longPressTimer.current = setTimeout(() => {
|
||||
menuProgress.value = withTiming(1, {
|
||||
duration: 300,
|
||||
easing: Easing.ease,
|
||||
});
|
||||
longPressTimer.current = null;
|
||||
}, 500);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onPanResponderEnd: (_evt, _gestureState) => {
|
||||
if (longPressTimer.current) {
|
||||
clearTimeout(longPressTimer.current);
|
||||
longPressTimer.current = null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
[menuProgress.value]
|
||||
);
|
||||
|
||||
const menuHeaderStyle = useMemo(
|
||||
() => ({
|
||||
...menuStyles.menuHeader,
|
||||
marginTop: insets.top + 10,
|
||||
}),
|
||||
[insets.top]
|
||||
);
|
||||
|
||||
if (state.isLoading) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Animated.View
|
||||
style={[styles.fill, animatedStyle]}
|
||||
pointerEvents="box-none"
|
||||
{...panResponder.panHandlers}
|
||||
>
|
||||
<Animated.View style={[styles.fillContent, appPointerEvents]}>
|
||||
{!state.showWebView ? (
|
||||
children
|
||||
) : (
|
||||
<WebView
|
||||
source={{ uri: getWebAppUrl() }}
|
||||
style={[styles.webView, { paddingTop: insets.top }]}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.menuContainer, menuAnimatedStyle]}>
|
||||
<View style={menuStyles.menuContainerStyle}>
|
||||
<View style={menuHeaderStyle}>
|
||||
<View style={menuStyles.leftSection}>
|
||||
<TouchableOpacity
|
||||
onPress={toggleWebView}
|
||||
style={menuStyles.button}
|
||||
>
|
||||
<MobileViewIcon
|
||||
color={state.showWebView ? "#7E7F80" : "#18191B"}
|
||||
/>
|
||||
{!state.showWebView && <ActiveDot />}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={toggleWebView}
|
||||
style={menuStyles.button}
|
||||
>
|
||||
<WebViewIcon color={state.showWebView ? "#18191B" : "#7E7F80"} />
|
||||
{state.showWebView && <ActiveDot />}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={menuStyles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={reloadApp}
|
||||
style={menuStyles.button}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={exitApp} style={menuStyles.button}>
|
||||
<CloseIcon />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
<InstructionsOverlay
|
||||
showTint={state.showTint}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
: ({ children }: { children: React.ReactNode }) => children;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
fillContent: {
|
||||
flex: 1,
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
},
|
||||
fill: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
menuContainer: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
},
|
||||
menuTouchable: {
|
||||
flex: 1,
|
||||
},
|
||||
bottomSheetBackground: {
|
||||
backgroundColor: "white",
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 20,
|
||||
},
|
||||
webViewContainer: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "#fff",
|
||||
zIndex: 2000,
|
||||
},
|
||||
webViewHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 18,
|
||||
backgroundColor: "#fff",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#e0e0e0",
|
||||
},
|
||||
webViewTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#18191B",
|
||||
},
|
||||
webViewCloseButton: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
webView: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const menuStyles = StyleSheet.create({
|
||||
menuContainerStyle: {
|
||||
backgroundColor: "#fff",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
menuHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 18,
|
||||
},
|
||||
appIcon: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
marginRight: 20,
|
||||
},
|
||||
appTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#18191B",
|
||||
flex: 1,
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 28,
|
||||
},
|
||||
button: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
leftSection: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 28,
|
||||
flex: 1,
|
||||
},
|
||||
holdTwoFingersTextContainer: {
|
||||
zIndex: 1,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
transform: [{ translateY: -24 }],
|
||||
},
|
||||
holdTwoFingersText: {
|
||||
fontSize: 28,
|
||||
color: "#fff",
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
|
||||
export default function Screen({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<AnythingMenu>{children}</AnythingMenu>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
106
apps/mobile/src/__create/fetch.ts
Normal file
106
apps/mobile/src/__create/fetch.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { fetch as expoFetch } from 'expo/fetch';
|
||||
|
||||
const originalFetch = fetch;
|
||||
const authKey = `${process.env.EXPO_PUBLIC_PROJECT_GROUP_ID}-jwt`;
|
||||
|
||||
const getURLFromArgs = (...args: Parameters<typeof fetch>) => {
|
||||
const [urlArg] = args;
|
||||
if (typeof urlArg === 'string') {
|
||||
return urlArg;
|
||||
}
|
||||
if (urlArg instanceof Request) {
|
||||
return urlArg.url;
|
||||
}
|
||||
// URL type may not be in the fetch signature for all TS environments
|
||||
if (typeof urlArg === 'object' && urlArg !== null && 'href' in urlArg) {
|
||||
return (urlArg as URL).href;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const isFileURL = (url: string) => {
|
||||
return url.startsWith('file://') || url.startsWith('data:');
|
||||
};
|
||||
|
||||
const isStaticAssetURL = (url: string) => {
|
||||
return /\.(wasm|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|otf|eot)(\?|$)/i.test(url);
|
||||
};
|
||||
|
||||
const isFirstPartyURL = (url: string) => {
|
||||
return (
|
||||
url.startsWith('/') ||
|
||||
(process.env.EXPO_PUBLIC_BASE_URL && url.startsWith(process.env.EXPO_PUBLIC_BASE_URL))
|
||||
);
|
||||
};
|
||||
|
||||
const isSecondPartyURL = (url: string) => {
|
||||
return url.startsWith('/_create/');
|
||||
};
|
||||
|
||||
type Params = Parameters<typeof expoFetch>;
|
||||
const fetchToWeb = async function fetchWithHeaders(...args: Params) {
|
||||
const firstPartyURL = process.env.EXPO_PUBLIC_BASE_URL;
|
||||
const secondPartyURL = process.env.EXPO_PUBLIC_PROXY_BASE_URL;
|
||||
if (!firstPartyURL || !secondPartyURL) {
|
||||
return expoFetch(...args);
|
||||
}
|
||||
const [input, init] = args;
|
||||
const url = getURLFromArgs(input, init);
|
||||
if (!url) {
|
||||
return expoFetch(input, init);
|
||||
}
|
||||
|
||||
if (isFileURL(url) || isStaticAssetURL(url)) {
|
||||
return originalFetch(input, init);
|
||||
}
|
||||
|
||||
const isExternalFetch = !isFirstPartyURL(url);
|
||||
// we should not add headers to requests that don't go to our own server
|
||||
if (isExternalFetch) {
|
||||
return expoFetch(input, init);
|
||||
}
|
||||
|
||||
let finalInput = input;
|
||||
const baseURL = isSecondPartyURL(url) ? secondPartyURL : firstPartyURL;
|
||||
if (typeof input === 'string') {
|
||||
finalInput = input.startsWith('/') ? `${baseURL}${input}` : input;
|
||||
} else {
|
||||
return expoFetch(input, init);
|
||||
}
|
||||
|
||||
const initHeaders = init?.headers ?? {};
|
||||
const finalHeaders = new Headers(initHeaders);
|
||||
|
||||
const headers = {
|
||||
'x-createxyz-project-group-id': process.env.EXPO_PUBLIC_PROJECT_GROUP_ID,
|
||||
host: process.env.EXPO_PUBLIC_HOST,
|
||||
'x-forwarded-host': process.env.EXPO_PUBLIC_HOST,
|
||||
'x-createxyz-host': process.env.EXPO_PUBLIC_HOST,
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (value) {
|
||||
finalHeaders.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const auth = await SecureStore.getItemAsync(authKey)
|
||||
.then((auth) => {
|
||||
return auth ? JSON.parse(auth) : null;
|
||||
})
|
||||
.catch(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
if (auth) {
|
||||
finalHeaders.set('authorization', `Bearer ${auth.jwt}`);
|
||||
}
|
||||
|
||||
return expoFetch(finalInput, {
|
||||
...init,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
};
|
||||
|
||||
export default fetchToWeb;
|
||||
20
apps/mobile/src/__create/placeholder.svg
Normal file
20
apps/mobile/src/__create/placeholder.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width={128} height={128} viewBox="0 0 895 895" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="895" height="895" rx="19" fill="#E9E7E7" />
|
||||
<g stroke="#C0C0C0" stroke-width="1.00975">
|
||||
<line x1="447.505" y1="-23" x2="447.505" y2="901" />
|
||||
<line x1="889.335" y1="447.505" x2="5.66443" y2="447.505" />
|
||||
<line x1="889.335" y1="278.068" x2="5.66443" y2="278.068" />
|
||||
<line x1="889.335" y1="57.1505" x2="5.66443" y2="57.1504" />
|
||||
<line x1="61.8051" y1="883.671" x2="61.8051" y2="0.000061" />
|
||||
<line x1="282.495" y1="907" x2="282.495" y2="-30" />
|
||||
<line x1="611.495" y1="907" x2="611.495" y2="-30" />
|
||||
<line x1="832.185" y1="883.671" x2="832.185" y2="0.000061" />
|
||||
<line x1="889.335" y1="827.53" x2="5.66443" y2="827.53" />
|
||||
<line x1="889.335" y1="606.613" x2="5.66443" y2="606.612" />
|
||||
<line x1="4.3568" y1="4.6428" x2="889.357" y2="888.643" />
|
||||
<line x1="-0.3568" y1="894.643" x2="894.643" y2="0.642772" />
|
||||
<circle cx="447.5" cy="441.5" r="163.995" />
|
||||
<circle cx="447.911" cy="447.911" r="237.407" />
|
||||
<circle cx="448" cy="442" r="384.495" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
apps/mobile/src/__create/polyfills.ts
Normal file
3
apps/mobile/src/__create/polyfills.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import updatedFetch from './fetch';
|
||||
// @ts-expect-error -- updatedFetch wraps the native fetch with custom headers
|
||||
global.fetch = updatedFetch;
|
||||
Reference in New Issue
Block a user