Skip to content

Commit 9d22556

Browse files
Merge upstream develop
2 parents 50ac499 + 629943b commit 9d22556

15 files changed

Lines changed: 632 additions & 237 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ tonic = { version = "0.14", default-features = false, features = ["codegen"] }
3131
tonic-prost = "0.14"
3232

3333
# Crypto / hashing
34-
ed25519-dalek = "2.2.0"
34+
ed25519-dalek = "3.0.0"
3535
sha2 = "0.11"
3636
hmac-sha256 = "1.1.12"
3737
getrandom = "0.4"

apps/polycentric/app/(tabs)/_layout.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
1+
import { usePolycentricContext } from '@/src/common/lib/polycentric-hooks';
12
import { useTheme } from '@/src/common/theme';
23
import { isIOS, isWeb } from '@/src/common/util/platform';
3-
import { Slot } from 'expo-router';
4+
import { Stack } from 'expo-router';
45
import { NativeTabs } from 'expo-router/unstable-native-tabs';
56

67
export default function TabsLayout() {
78
const { theme } = useTheme();
9+
const { currentIdentity, isLoading, isReady } = usePolycentricContext();
10+
11+
// Stay permissive until the identity store has settled — pruning routes
12+
// during startup would break deep links that resolve after login state.
13+
const accountGuard = isLoading || !isReady || !!currentIdentity;
14+
815
if (isWeb) {
9-
return <Slot />;
16+
// Web has no visible tab bar (the sidebar in Layout.tsx is the nav);
17+
// a navigator is used instead of a plain <Slot/> so the account-only
18+
// routes can be route-guarded. Guarded routes are removed from
19+
// navigation while logged out; explore/search stay public.
20+
return (
21+
<Stack
22+
screenOptions={{
23+
headerShown: false,
24+
animation: 'none',
25+
}}
26+
>
27+
<Stack.Protected guard={accountGuard}>
28+
<Stack.Screen name="feed" />
29+
<Stack.Screen name="notifications" />
30+
<Stack.Screen name="verifications" />
31+
<Stack.Screen name="compose" />
32+
<Stack.Screen name="profile" />
33+
<Stack.Screen name="claims" />
34+
</Stack.Protected>
35+
<Stack.Screen name="explore" />
36+
<Stack.Screen name="search" />
37+
</Stack>
38+
);
1039
}
1140

1241
return (

apps/polycentric/app/_layout.tsx

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Toaster } from '@/src/common/components/toast';
22
import { LinkPreviewsProvider } from '@/src/common/link-previews';
3-
import { PolycentricProvider } from '@/src/common/lib/polycentric-hooks';
3+
import {
4+
PolycentricProvider,
5+
usePolycentricContext,
6+
} from '@/src/common/lib/polycentric-hooks';
47
import { Atoms, ThemeProvider, useTheme } from '@/src/common/theme';
58
import { isWeb } from '@/src/common/util/platform';
69
import '@/src/common/util/react-native-screens-feature-flags';
@@ -26,6 +29,11 @@ export const unstable_settings = {
2629

2730
function RootStack() {
2831
const { theme } = useTheme();
32+
const { currentIdentity, isLoading, isReady } = usePolycentricContext();
33+
34+
// Stay permissive until the identity store has settled — pruning routes
35+
// during startup would break deep links that resolve after login state.
36+
const accountGuard = isLoading || !isReady || !!currentIdentity;
2937

3038
const stack = (
3139
<>
@@ -40,25 +48,41 @@ function RootStack() {
4048
: { orientation: 'portrait_up' }),
4149
}}
4250
>
43-
<Stack.Screen name="(tabs)" />
51+
{/* Mobile requires an account for the tabs (feeds, notifications,
52+
...); deep-linked profile/post views stay public everywhere. On
53+
web the tabs stay routable and get granular guards instead. */}
54+
<Stack.Protected guard={isWeb || accountGuard}>
55+
<Stack.Screen name="(tabs)" />
56+
</Stack.Protected>
57+
4458
<Stack.Screen name="(onboarding)" />
45-
<Stack.Screen
46-
name="feed"
47-
options={{
48-
presentation: 'transparentModal',
49-
animation: isWeb ? 'fade' : 'default',
50-
contentStyle: { backgroundColor: 'transparent' },
51-
}}
52-
/>
53-
<Stack.Screen name="settings" />
54-
<Stack.Screen
55-
name="[identityId]/edit"
56-
options={{
57-
presentation: 'transparentModal',
58-
animation: 'none',
59-
contentStyle: { backgroundColor: 'transparent' },
60-
}}
61-
/>
59+
60+
{/* Account-only on every platform. Routes are auto-registered from
61+
the filesystem; the explicit declarations here exist to place
62+
them inside the guard. */}
63+
<Stack.Protected guard={accountGuard}>
64+
<Stack.Screen
65+
name="feed"
66+
options={{
67+
presentation: 'transparentModal',
68+
animation: isWeb ? 'fade' : 'default',
69+
contentStyle: { backgroundColor: 'transparent' },
70+
}}
71+
/>
72+
<Stack.Screen name="settings" />
73+
<Stack.Screen
74+
name="[identityId]/edit"
75+
options={{
76+
presentation: 'transparentModal',
77+
animation: 'none',
78+
contentStyle: { backgroundColor: 'transparent' },
79+
}}
80+
/>
81+
<Stack.Screen name="identity/switch" />
82+
<Stack.Screen name="verifications/index" />
83+
<Stack.Screen name="verifications/claim" />
84+
</Stack.Protected>
85+
6286
<Stack.Screen
6387
name="image-viewer"
6488
options={{

apps/polycentric/src/features/profile/EditProfileScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function EditProfileSheet({ identityKey }: { identityKey: string }) {
9393
<Text variant="small" color="neutral_500">
9494
ALIAS
9595
</Text>
96-
<InfoTooltip text="An alias like you@yourdomain.com that points to this profile. Your domain must be set up to link back here before it can be saved." />
96+
<InfoTooltip text="An alias like you@yourdomain.com (or just yourdomain.com) that points to this profile. Your domain must be set up to link back here before it can be saved." />
9797
</View>
9898
<TextInput
9999
value={edit.aliasDraft}

apps/polycentric/src/features/profile/ProfileScreen.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const ALIAS = 'test@domain.com';
1111
jest.mock('@polycentric/react-native', () => ({
1212
FetchMode: { Default: 'Default' },
1313
resolveAlias: jest.fn(),
14+
isIdentityKey: (s: string): boolean =>
15+
s.length > 0 && [...s].every((c) => '0123456789abcdefABCDEF'.includes(c)),
1416
normalizeAlias: (alias: string): string | null => {
1517
let s = alias.trim();
1618
if (s.startsWith('@')) s = s.slice(1);

apps/polycentric/src/features/profile/ProfileScreen.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Atoms, useTheme } from '@/src/common/theme';
55
import { useIdentityFeed } from '@/src/features/feed/hooks/useIdentityFeed';
66
import {
77
FetchMode,
8+
isIdentityKey,
89
normalizeAlias,
910
resolveAlias,
1011
} from '@polycentric/react-native';
@@ -40,13 +41,12 @@ export default function ProfileScreen({
4041
}) {
4142
const { identityId } = useLocalSearchParams<{ identityId: string }>();
4243

43-
// An alias (user@domain) rather than a polycentric identity key —
44-
// resolve it to a key first.
45-
if (identityId?.includes('@')) {
46-
return <AliasProfile alias={identityId} tab={tab} />;
44+
if (!identityId || isIdentityKey(identityId)) {
45+
return <IdentityProfile identityKey={identityId ?? null} tab={tab} />;
4746
}
4847

49-
return <IdentityProfile identityKey={identityId ?? null} tab={tab} />;
48+
// An alias (user@domain, or a bare domain) — resolve it to a key first.
49+
return <AliasProfile alias={identityId} tab={tab} />;
5050
}
5151

5252
/**

apps/polycentric/src/features/profile/search/useProfileSuggestions.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,26 @@ describe('useProfileSuggestions', () => {
180180
]);
181181
});
182182

183+
it('resolves a bare-domain query as a wildcard alias', async () => {
184+
const stranger = 'ef'.repeat(32);
185+
mockResolve.mockResolvedValue(stranger);
186+
187+
const { result } = renderSuggestions('Example.com');
188+
await act(async () => {
189+
jest.advanceTimersByTime(300);
190+
});
191+
192+
expect(mockResolve).toHaveBeenCalledWith('example.com');
193+
expect(result.current.suggestions).toEqual([
194+
{
195+
identity: stranger,
196+
name: null,
197+
alias: 'example.com',
198+
source: 'alias',
199+
},
200+
]);
201+
});
202+
183203
it('shows nothing for an alias that does not resolve', async () => {
184204
mockResolve.mockResolvedValue(null);
185205

apps/polycentric/src/features/profile/search/useProfileSuggestions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ function isHex(s: string): boolean {
4040
* Suggests identities for a partially-typed profile query:
4141
* - people the current user follows, matched on profile name, alias, or
4242
* identity-id prefix (everyone followed when the query is empty),
43-
* - the identity behind a `user@domain` alias, resolved over the network,
43+
* - the identity behind a `user@domain` or bare-domain alias, resolved over
44+
* the network,
4445
* - the query itself when it's a pasted identity id.
4546
*
4647
* TODO: matching runs client-side over the first FOLLOWING_LIMIT follow
@@ -63,7 +64,7 @@ export function useProfileSuggestions(
6364
const profiles = useProfiles(followedIdentities);
6465

6566
const trimmed = query.trim();
66-
const aliasQuery = trimmed.includes('@') ? normalizeAlias(trimmed) : null;
67+
const aliasQuery = normalizeAlias(trimmed);
6768

6869
// Debounced alias → identity resolution; the result remembers which alias
6970
// it answered so a stale response never surfaces for a newer query.

docs/content/setting-up-an-alias.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Three requirements:
4343
- It must be reachable over **HTTPS**.
4444

4545
You can list several names in the same file if you host aliases for more than
46-
one person.
46+
one person. You can also use the name "*" to specify the identity that can use the bare domain alias (i.e. simply @domain.com instead of @user@domain.com).
4747

4848
## 2. Set the alias on your profile
4949

0 commit comments

Comments
 (0)