Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,25 @@ const config: ExpoConfig = {
},
],
['react-native-appsflyer', { shouldUsePurchaseConnector: true }],
// Local wrapper: pnpm isolation + Kilo target-name collision (see plugin).
[
'./plugins/withExpoShareIntent',
{
iosActivationRules: {
NSExtensionActivationSupportsText: true,
NSExtensionActivationSupportsWebURLWithMaxCount: 1,
NSExtensionActivationSupportsWebPageWithMaxCount: 1,
NSExtensionActivationSupportsImageWithMaxCount: 5,
NSExtensionActivationSupportsFileWithMaxCount: 5,
},
androidIntentFilters: ['text/*', '*/*'],
androidMultiIntentFilters: ['*/*'],
iosAppGroupIdentifier: 'group.com.kilocode.kiloapp',
// Display name "Kilo" is applied by the wrapper; target is ShareExtension
// because iosShareExtensionName "Kilo" collides with the main app target.
iosShareExtensionName: 'Kilo',
},
],
'./plugins/withAndroidManifestFix',
// ponytail: only registered when GOOGLE_IOS_CLIENT_ID is set, so prebuild works before the
// Google OAuth clients exist.
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"expo-router": "55.0.16",
"expo-screen-corner-radius": "^1.0.1",
"expo-secure-store": "55.0.15",
"expo-share-intent": "^6.1.1",
"expo-sharing": "~55.0.21",
"expo-speech-recognition": "3.1.3",
"expo-splash-screen": "55.0.22",
Expand Down
88 changes: 88 additions & 0 deletions apps/mobile/plugins/withExpoShareIntent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const fs = require('fs');
const Module = require('module');
const path = require('path');

const { withFinalizedMod } = require('expo/config-plugins');

// expo-share-intent@6.1.1 requires @expo/plist without declaring it. Under pnpm's
// isolated linker the nested plugin cannot resolve that package from its own
// directory. Resolve the copy that ships with @expo/config-plugins and patch
// Module resolution only while loading the upstream plugin.
const expoConfigPluginsEntry = require.resolve('expo/config-plugins');
const configPluginsPkg = require.resolve('@expo/config-plugins/package.json', {
paths: [path.dirname(expoConfigPluginsEntry)],
});
const expoPlistEntry = require.resolve('@expo/plist', {
paths: [path.dirname(configPluginsPkg)],
});

function loadUpstreamPlugin() {
const originalResolveFilename = Module._resolveFilename;
Module._resolveFilename = function resolveWithPlistFallback(request, parent, isMain, options) {
if (request === '@expo/plist') {
return expoPlistEntry;
}
return originalResolveFilename.call(this, request, parent, isMain, options);
};
try {
// Clear cache so a previous failed load does not stick.
const resolved = require.resolve('expo-share-intent/app.plugin.js');
delete require.cache[resolved];
const mod = require(resolved);
return typeof mod === 'function' ? mod : mod.default;
} finally {
Module._resolveFilename = originalResolveFilename;
}
}

const withExpoShareIntentUpstream = loadUpstreamPlugin();

// Upstream uses iosShareExtensionName for both the Xcode target name and
// CFBundleDisplayName. The app target is already "Kilo", so "Kilo" collides and
// the extension target is skipped. Keep a distinct target name and force the
// share-sheet label to "Kilo" after files are written.
const SHARE_EXTENSION_TARGET = 'ShareExtension';
const SHARE_SHEET_DISPLAY_NAME = 'Kilo';

function withKiloShareSheetDisplayName(config) {
// finalized runs after dangerous mods that write ShareExtension-Info.plist.
return withFinalizedMod(config, [
'ios',
async config => {
const infoPath = path.join(
config.modRequest.platformProjectRoot,
SHARE_EXTENSION_TARGET,
'ShareExtension-Info.plist'
);
if (!fs.existsSync(infoPath)) {
throw new Error(
`Expected ShareExtension-Info.plist so CFBundleDisplayName can be rewritten to "${SHARE_SHEET_DISPLAY_NAME}", but it was missing at ${infoPath}`
);
}
const original = fs.readFileSync(infoPath, 'utf8');
const updated = original.replace(
/(<key>CFBundleDisplayName<\/key>\s*<string>)[^<]*(<\/string>)/,
`$1${SHARE_SHEET_DISPLAY_NAME}$2`
);
if (updated === original) {
throw new Error(
`Failed to rewrite CFBundleDisplayName to "${SHARE_SHEET_DISPLAY_NAME}" in ${infoPath}`
);
}
fs.writeFileSync(infoPath, updated);
return config;
},
]);
}

module.exports = function withExpoShareIntent(config, props = {}) {
const { iosShareExtensionName: _ignoredDisplayName, ...rest } = props;
const parameters = {
...rest,
// Distinct from the main "Kilo" app target (see collision note above).
iosShareExtensionName: SHARE_EXTENSION_TARGET,
};
config = withExpoShareIntentUpstream(config, parameters);
config = withKiloShareSheetDisplayName(config);
return config;
};
11 changes: 11 additions & 0 deletions apps/mobile/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Stack } from 'expo-router';
import { UserWebConnectionProvider } from '@/components/agents/user-web-connection-provider';
import { KiloChatPresenceMount } from '@/components/kilo-chat/kilo-chat-presence-mount';
import { KiloChatProvider } from '@/components/kilo-chat/kilo-chat-provider';
import { SharePayloadNavigator } from '@/components/share/share-payload-navigator';
import { ActiveSessionsLiveSyncMount } from '@/lib/active-sessions-live-sync-mount';
import { useFormSheetDetents } from '@/lib/form-sheet';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
Expand All @@ -15,6 +16,7 @@ export default function AppLayout() {
return (
<UserWebConnectionProvider>
<ActiveSessionsLiveSyncMount />
<SharePayloadNavigator />
<KiloChatProvider>
<KiloChatPresenceMount>
<StoreKiloPassPurchaseProvider>
Expand Down Expand Up @@ -70,6 +72,15 @@ export default function AppLayout() {
headerShown: false,
}}
/>
<Stack.Screen
name="share-gate"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, fullSheetDetent],
sheetGrabberVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
name="kilo-pass"
options={{
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/src/app/(app)/agent-chat/[session-id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function SessionDetailScreen() {
organizationId: routeOrganizationId,
via,
spawned,
shareId: shareIdParam,
} = useLocalSearchParams<{
'session-id': string;
organizationId?: string;
Expand All @@ -36,7 +37,10 @@ export default function SessionDetailScreen() {
* shows the same permanent state it always did.
*/
spawned?: string;
shareId?: string;
}>();
// Param can be string | string[] depending on how the route was opened.
const shareId = Array.isArray(shareIdParam) ? shareIdParam[0] : shareIdParam;
const trpc = useTRPC();
const router = useRouter();
const sessionQuery = useQuery({
Expand Down Expand Up @@ -123,6 +127,7 @@ export default function SessionDetailScreen() {
<SessionDetailContent
sessionId={sessionId as KiloSessionId}
openedVia={via === 'push' ? 'push' : 'app'}
shareId={shareId}
/>
</AgentSessionProvider>
);
Expand Down
Loading