Skip to content
Open
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
1 change: 1 addition & 0 deletions examples/test-app/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
buildCacheProvider: { plugin: 'expo-build-disk-cache' },
plugins: [
'expo-router',
'expo-secure-store',
[
'expo-audio',
{
Expand Down
1 change: 1 addition & 0 deletions examples/test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"expo-linking": "56.0.14",
"expo-modules-core": "56.0.17",
"expo-router": "~56.2.11",
"expo-secure-store": "~56.0.4",
"expo-status-bar": "~56.0.4",
"react": "19.2.3",
"react-dom": "19.2.3",
Expand Down
12 changes: 12 additions & 0 deletions examples/test-app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions examples/test-app/src/screens/AutomationLabScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@ import {
} from 'react-native';
import { getRecordingPermissionsAsync, requestRecordingPermissionsAsync } from 'expo-audio';
import { requireOptionalNativeModule } from 'expo-modules-core';
import * as SecureStore from 'expo-secure-store';

import { ActionButton, ScreenTitle, SectionCard } from '../components';
import { useAppColors, type AppColors } from '../theme';

// A fixed key/value pair standing in for a real login token: this screen only
// needs to prove keychain-backed state survives `clear-app-state` but not
// `reset-keychain`, not to model an actual auth flow.
const KEYCHAIN_AUTH_KEY = 'automation-keychain-auth-token';
const KEYCHAIN_AUTH_VALUE = 'demo-auth-token';

type PushBroadcastLabModule = {
lastPushBroadcast(): string;
};
Expand Down Expand Up @@ -45,6 +52,7 @@ export function AutomationLabScreen(props: {
const [microphonePermission, setMicrophonePermission] = useState('checking');
const [lastPushBroadcast, setLastPushBroadcast] = useState('none');
const [sheetVisible, setSheetVisible] = useState(false);
const [keychainAuthStatus, setKeychainAuthStatus] = useState('checking');
const permissionReadGeneration = useRef(0);
const windowMode = dimensions.width > dimensions.height ? 'landscape' : 'portrait';

Expand Down Expand Up @@ -125,6 +133,26 @@ export function AutomationLabScreen(props: {
setLastPushBroadcast(pushBroadcastLab?.lastPushBroadcast() ?? 'unavailable');
}

useEffect(() => {
let mounted = true;
void SecureStore.getItemAsync(KEYCHAIN_AUTH_KEY)
.then((value) => {
if (mounted)
setKeychainAuthStatus(value === KEYCHAIN_AUTH_VALUE ? 'signed-in' : 'signed-out');
})
.catch(() => {
if (mounted) setKeychainAuthStatus('error');
});
return () => {
mounted = false;
};
}, []);

async function signInWithKeychain() {
await SecureStore.setItemAsync(KEYCHAIN_AUTH_KEY, KEYCHAIN_AUTH_VALUE);
setKeychainAuthStatus('signed-in');
}

return (
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
<ScreenTitle
Expand Down Expand Up @@ -235,6 +263,19 @@ export function AutomationLabScreen(props: {
/>
</SectionCard>

<SectionCard title="Keychain-backed auth">
<ActionButton
label="Sign in (write keychain)"
onPress={() => void signInWithKeychain()}
testID="automation-keychain-signin"
/>
<StateRow
label="Auth status"
testID="automation-keychain-status"
value={keychainAuthStatus}
/>
</SectionCard>

<SectionCard title="Android push broadcast">
<ActionButton
kind="secondary"
Expand Down
4 changes: 4 additions & 0 deletions packages/contracts/src/client-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export type SettingsUpdateOptions =
state: 'clear';
app?: string;
})
| (DeviceCommandBaseOptions & {
setting: 'reset-keychain';
state: 'clear';
})
| (DeviceCommandBaseOptions & {
setting: 'wifi' | 'airplane' | 'location';
state: 'on' | 'off';
Expand Down
4 changes: 3 additions & 1 deletion packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const SETTINGS_FACEID_USAGE = 'faceid <match|nonmatch|enroll|unenroll>';
const SETTINGS_TOUCHID_USAGE = 'touchid <match|nonmatch|enroll|unenroll>';
const SETTINGS_FINGERPRINT_USAGE = 'fingerprint <match|nonmatch>';
const SETTINGS_CLEAR_APP_STATE_USAGE = 'clear-app-state [app-id]';
const SETTINGS_RESET_KEYCHAIN_USAGE = 'reset-keychain clear';
const SETTINGS_PERMISSION_USAGE =
'permission <grant|deny|reset> <camera|microphone|photos|contacts|contacts-limited|notifications|calendar|location|location-always|media-library|motion|reminders|siri> [full|limited]';
const SETTINGS_MACOS_PERMISSION_USAGE =
Expand All @@ -60,11 +61,12 @@ export const SETTINGS_USAGE_OVERRIDE = [
`settings ${SETTINGS_TOUCHID_USAGE}`,
`settings ${SETTINGS_FINGERPRINT_USAGE}`,
`settings ${SETTINGS_CLEAR_APP_STATE_USAGE}`,
`settings ${SETTINGS_RESET_KEYCHAIN_USAGE}`,
`settings ${SETTINGS_PERMISSION_USAGE}`,
`settings ${SETTINGS_MACOS_PERMISSION_USAGE}`,
].join(' | ');

export const SETTINGS_INVALID_ARGS_MESSAGE = `settings requires ${SETTINGS_WIFI_USAGE}, ${SETTINGS_LOCATION_SET_USAGE}, ${SETTINGS_ANIMATIONS_USAGE}, ${SETTINGS_APPEARANCE_USAGE}, ${SETTINGS_FACEID_USAGE}, ${SETTINGS_TOUCHID_USAGE}, ${SETTINGS_FINGERPRINT_USAGE}, ${SETTINGS_CLEAR_APP_STATE_USAGE}, ${SETTINGS_PERMISSION_USAGE}, or ${SETTINGS_MACOS_PERMISSION_USAGE}`;
export const SETTINGS_INVALID_ARGS_MESSAGE = `settings requires ${SETTINGS_WIFI_USAGE}, ${SETTINGS_LOCATION_SET_USAGE}, ${SETTINGS_ANIMATIONS_USAGE}, ${SETTINGS_APPEARANCE_USAGE}, ${SETTINGS_FACEID_USAGE}, ${SETTINGS_TOUCHID_USAGE}, ${SETTINGS_FINGERPRINT_USAGE}, ${SETTINGS_CLEAR_APP_STATE_USAGE}, ${SETTINGS_RESET_KEYCHAIN_USAGE}, ${SETTINGS_PERMISSION_USAGE}, or ${SETTINGS_MACOS_PERMISSION_USAGE}`;

export function isMacOsSettingSupported(setting: string): boolean {
const normalized = setting.trim().toLowerCase();
Expand Down
Loading
Loading