Skip to content

Commit 152f6fa

Browse files
committed
Merge branch 'main' into feat/mobile-phase-8-housekeeping
2 parents 3057341 + 68c9526 commit 152f6fa

9 files changed

Lines changed: 156 additions & 16 deletions

File tree

bun.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mobile/app.json

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,57 @@
99
"userInterfaceStyle": "automatic",
1010
"plugins": [
1111
"expo-secure-store",
12-
"expo-notifications"
12+
"expo-notifications",
13+
[
14+
"expo-build-properties",
15+
{
16+
"android": {
17+
"minSdkVersion": 24,
18+
"compileSdkVersion": 35
19+
}
20+
}
21+
]
1322
],
1423
"android": {
1524
"package": "com.enmarch.mobile",
1625
"adaptiveIcon": {
1726
"backgroundColor": "#0f172a"
1827
},
19-
"edgeToEdgeEnabled": true
28+
"edgeToEdgeEnabled": true,
29+
"intentFilters": [
30+
{
31+
"action": "VIEW",
32+
"autoVerify": true,
33+
"data": [
34+
{
35+
"scheme": "enmarch",
36+
"host": "invite"
37+
},
38+
{
39+
"scheme": "https",
40+
"host": "enmarch.app",
41+
"pathPrefix": "/invite"
42+
}
43+
],
44+
"category": [
45+
"BROWSABLE",
46+
"DEFAULT"
47+
]
48+
}
49+
],
50+
"queries": {
51+
"intent": [
52+
{
53+
"action": "VIEW",
54+
"category": [
55+
"BROWSABLE"
56+
],
57+
"data": {
58+
"scheme": "solana-wallet"
59+
}
60+
}
61+
]
62+
}
2063
}
2164
}
2265
}

mobile/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"expo-notifications": "*",
3535
"expo-secure-store": "*",
3636
"expo-clipboard": "*",
37+
"expo-build-properties": "~1.0.9",
3738
"expo-status-bar": "*",
3839
"nativewind": "^4.2.1",
3940
"phosphor-react-native": "^3.0.4",

mobile/src/hooks/use-conversations.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
listConversations,
1111
saveConversation,
1212
} from "@/adapters/conversation-store";
13+
import { extractInviteCode } from "@/lib/invite-links";
1314
import { messengerKeys } from "@/lib/messenger-query-keys";
1415
import { useSolanaConnection } from "@/state/solana-connection-context";
1516
import { readRegisteredXwingPubkey } from "@enmarch/solana-client";
@@ -79,7 +80,7 @@ export function useConversations(input: UseConversationsInput) {
7980

8081
const conversation = importConversationInvite(
8182
input.walletAddress,
82-
inviteCode.trim(),
83+
extractInviteCode(inviteCode),
8384
);
8485
const registeredPubkey = await readRegisteredXwingPubkey(
8586
connection,

mobile/src/lib/invite-links.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const CUSTOM_INVITE_PREFIX = "enmarch://invite/";
2+
const HTTPS_INVITE_PREFIX = "https://enmarch.app/invite/";
3+
4+
function safeDecodeURIComponent(value: string) {
5+
try {
6+
return decodeURIComponent(value);
7+
} catch {
8+
return value;
9+
}
10+
}
11+
12+
export function buildInviteDeepLink(inviteCode: string) {
13+
return `${CUSTOM_INVITE_PREFIX}${encodeURIComponent(inviteCode)}`;
14+
}
15+
16+
export function extractInviteCode(input: string) {
17+
const trimmed = input.trim();
18+
19+
if (!trimmed) {
20+
return "";
21+
}
22+
23+
const directMatch = trimmed.match(
24+
/(enmarch:\/\/invite\/|https:\/\/enmarch\.app\/invite\/)(\S+)/i,
25+
);
26+
27+
if (directMatch?.[2]) {
28+
return safeDecodeURIComponent(directMatch[2]);
29+
}
30+
31+
if (trimmed.startsWith(CUSTOM_INVITE_PREFIX)) {
32+
return safeDecodeURIComponent(trimmed.slice(CUSTOM_INVITE_PREFIX.length));
33+
}
34+
35+
if (trimmed.startsWith(HTTPS_INVITE_PREFIX)) {
36+
return safeDecodeURIComponent(trimmed.slice(HTTPS_INVITE_PREFIX.length));
37+
}
38+
39+
return safeDecodeURIComponent(trimmed);
40+
}
41+
42+
export function formatInviteShareMessage(inviteCode: string) {
43+
const inviteLink = buildInviteDeepLink(inviteCode);
44+
45+
return `Join my secure room on Enmarch:\n\n${inviteLink}\n\nInvite code:\n${inviteCode}`;
46+
}

mobile/src/navigation/root-navigator.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
import {
22
DarkTheme,
3+
type LinkingOptions,
34
NavigationContainer,
45
type Theme,
56
} from "@react-navigation/native";
67
import { createNativeStackNavigator } from "@react-navigation/native-stack";
78

89
import { AppTabs } from "@/navigation/tabs";
10+
import type { RootStackParamList } from "@/navigation/types";
911
import { ConnectScreen } from "@/screens/connect-screen";
1012
import { useAppBootstrap } from "@/state/app-bootstrap-context";
1113
import { colors } from "@/theme/colors";
1214

13-
type RootStackParamList = {
14-
Connect: undefined;
15-
AppTabs: undefined;
16-
};
17-
1815
const RootStack = createNativeStackNavigator<RootStackParamList>();
1916

2017
const navigationTheme: Theme = {
@@ -30,6 +27,27 @@ const navigationTheme: Theme = {
3027
},
3128
};
3229

30+
const linking: LinkingOptions<RootStackParamList> = {
31+
prefixes: ["enmarch://", "https://enmarch.app"],
32+
config: {
33+
screens: {
34+
Connect: "connect",
35+
AppTabs: {
36+
screens: {
37+
Chats: {
38+
screens: {
39+
ChatList: "",
40+
ChatThread: "room/:conversationId",
41+
NewChat: "invite/:code",
42+
},
43+
},
44+
Settings: "settings",
45+
},
46+
},
47+
},
48+
},
49+
};
50+
3351
export function RootNavigator() {
3452
const { appReady, wallet } = useAppBootstrap();
3553

@@ -38,7 +56,7 @@ export function RootNavigator() {
3856
}
3957

4058
return (
41-
<NavigationContainer theme={navigationTheme}>
59+
<NavigationContainer linking={linking} theme={navigationTheme}>
4260
<RootStack.Navigator screenOptions={{ headerShown: false, animation: "fade" }}>
4361
{appReady ? (
4462
<RootStack.Screen name="AppTabs" component={AppTabs} />

mobile/src/navigation/types.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import type { NavigatorScreenParams } from "@react-navigation/native";
2+
13
export type ChatStackParamList = {
24
ChatList: undefined;
35
ChatThread: { conversationId: string };
4-
NewChat: undefined;
6+
NewChat: { code?: string } | undefined;
57
};
68

79
export type AppTabsParamList = {
8-
Chats: undefined;
10+
Chats: NavigatorScreenParams<ChatStackParamList> | undefined;
911
Settings: undefined;
1012
};
13+
14+
export type RootStackParamList = {
15+
Connect: undefined;
16+
AppTabs: NavigatorScreenParams<AppTabsParamList> | undefined;
17+
};

mobile/src/screens/chat-thread-screen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useConversationMessages } from "@/hooks/use-conversation-messages";
2121
import { getConversationInviteCode } from "@/hooks/use-conversations";
2222
import { useRelayTransport } from "@/hooks/use-relay-transport";
2323
import { formatWallet } from "@/lib/format";
24+
import { formatInviteShareMessage } from "@/lib/invite-links";
2425
import type { ChatStackParamList } from "@/navigation/types";
2526
import { colors } from "@/theme/colors";
2627

@@ -108,7 +109,7 @@ export function ChatThreadScreen(props: Props) {
108109

109110
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
110111
await Share.share({
111-
message: `Join my secure room on Enmarch:\n\n${getConversationInviteCode(activeConversation)}`,
112+
message: formatInviteShareMessage(getConversationInviteCode(activeConversation)),
112113
title: "Enmarch Invite",
113114
}).catch(() => null);
114115
};

mobile/src/screens/new-chat-screen.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useEffect, useState } from "react";
22
import {
33
ActivityIndicator,
44
KeyboardAvoidingView,
@@ -16,6 +16,7 @@ import * as Clipboard from "expo-clipboard";
1616
import * as Haptics from "expo-haptics";
1717

1818
import { useChatAppState } from "@/hooks/use-chat-app-state";
19+
import { extractInviteCode, formatInviteShareMessage } from "@/lib/invite-links";
1920
import type { ChatStackParamList } from "@/navigation/types";
2021
import { colors } from "@/theme/colors";
2122

@@ -27,6 +28,17 @@ export function NewChatScreen(props: Props) {
2728
const [inviteCode, setInviteCode] = useState("");
2829
const [error, setError] = useState<string | null>(null);
2930

31+
useEffect(() => {
32+
const routeInviteCode = props.route.params?.code;
33+
34+
if (!routeInviteCode) {
35+
return;
36+
}
37+
38+
setInviteCode(extractInviteCode(routeInviteCode));
39+
setError(null);
40+
}, [props.route.params?.code]);
41+
3042
const canImport = inviteCode.trim().length > 10;
3143
const dismiss = () => props.navigation.goBack();
3244

@@ -43,7 +55,7 @@ export function NewChatScreen(props: Props) {
4355
const result = await conversations.createInvite(label);
4456

4557
await Share.share({
46-
message: `Join my secure room on Enmarch:\n\n${result.inviteCode}`,
58+
message: formatInviteShareMessage(result.inviteCode),
4759
title: "Enmarch Invite",
4860
}).catch(() => null);
4961

@@ -84,7 +96,7 @@ export function NewChatScreen(props: Props) {
8496
try {
8597
const text = await Clipboard.getStringAsync();
8698
if (text) {
87-
setInviteCode(text.trim());
99+
setInviteCode(extractInviteCode(text));
88100
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
89101
}
90102
} catch {
@@ -310,7 +322,7 @@ export function NewChatScreen(props: Props) {
310322
setInviteCode(text);
311323
setError(null);
312324
}}
313-
placeholder="Paste invite code"
325+
placeholder="Paste invite code or deep link"
314326
placeholderTextColor={colors.mutedForeground}
315327
multiline
316328
style={{

0 commit comments

Comments
 (0)