@ateam-ai-mobile/ui 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +31 -36
- package/dist/index.d.ts +31 -36
- package/dist/index.js +96 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +127 -49
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/auth/HandoffScreen.tsx +3 -11
- package/src/auth/LoginScreen.tsx +68 -17
- package/src/auth/OpenJoinScreen.tsx +4 -6
- package/src/auth/handoffExchange.ts +6 -9
- package/src/auth/login.ts +5 -10
- package/src/auth/openJoin.ts +5 -8
- package/src/index.ts +3 -1
- package/src/plugin/EmbeddedPluginWebView.tsx +4 -1
- package/src/providers.ts +22 -0
- package/src/voice/voiceSession.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ateam-ai-mobile/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Prebuilt ADAS chat experience for React Native — full-screen chat, chips, widget views. Built on @ateam-ai-mobile/core + @ateam-ai-mobile/plugin-runtime.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"build": "tsup"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@ateam-ai-mobile/core": "0.1.
|
|
13
|
-
"@ateam-ai-mobile/plugin-runtime": "0.1.
|
|
12
|
+
"@ateam-ai-mobile/core": "0.1.11",
|
|
13
|
+
"@ateam-ai-mobile/plugin-runtime": "0.1.11",
|
|
14
14
|
"zustand": "^5.0.0"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Everything host-specific is injected:
|
|
11
11
|
* - `code` the deep-link code (capture + parsing stay host-side)
|
|
12
|
-
* - `onAdoptTenant` the host's SINGLE tenant writer, AWAITED before onSignedIn
|
|
13
|
-
* so ACTIVE_TENANT can't drift on a cross-tenant handoff
|
|
14
12
|
* - `onSignedIn` session adoption (host: authStore.login → core session +
|
|
15
13
|
* the shell-only tenantConfirmed / phase gate)
|
|
16
14
|
* - `onDone` clear the consumed single-use code (host store) after success
|
|
@@ -31,12 +29,6 @@ const fontFam = Platform.OS === 'ios' ? 'SF Pro Rounded' : undefined;
|
|
|
31
29
|
export interface HandoffScreenProps {
|
|
32
30
|
/** The one-time handoff code the host captured from the deep link (null = nothing pending). */
|
|
33
31
|
code: string | null;
|
|
34
|
-
/**
|
|
35
|
-
* Adopt the handed-off tenant. AWAITED before onSignedIn so the host's active
|
|
36
|
-
* tenant is set before the session reads it (the exchange result carries the
|
|
37
|
-
* tenant the code was bound to). No-op is fine when the result has no tenant.
|
|
38
|
-
*/
|
|
39
|
-
onAdoptTenant: (tenant: string) => void | Promise<void>;
|
|
40
32
|
/** Adopt the session JWT (host wires its authStore.login → core session + tenantConfirmed). */
|
|
41
33
|
onSignedIn: (jwt: string) => void | Promise<void>;
|
|
42
34
|
/** Clear the consumed single-use code so a relaunch never retries it. Called after success. */
|
|
@@ -45,7 +37,7 @@ export interface HandoffScreenProps {
|
|
|
45
37
|
onCancel: () => void;
|
|
46
38
|
}
|
|
47
39
|
|
|
48
|
-
export default function HandoffScreen({ code,
|
|
40
|
+
export default function HandoffScreen({ code, onSignedIn, onDone, onCancel }: HandoffScreenProps) {
|
|
49
41
|
const [err, setErr] = useState<string | null>(null);
|
|
50
42
|
// Exchange each code AT MOST ONCE. A single-use code exchanged twice
|
|
51
43
|
// (StrictMode/fast-refresh double-invoke) burns itself: the first call wins and
|
|
@@ -57,9 +49,9 @@ export default function HandoffScreen({ code, onAdoptTenant, onSignedIn, onDone,
|
|
|
57
49
|
useEffect(() => {
|
|
58
50
|
if (!code || startedForRef.current === code) return;
|
|
59
51
|
startedForRef.current = code;
|
|
60
|
-
runHandoffExchange(code, { exchange: exchangeHandoff,
|
|
52
|
+
runHandoffExchange(code, { exchange: exchangeHandoff, onSignedIn, onDone })
|
|
61
53
|
.catch((e: any) => setErr(handoffErrorMessage(e?.message)));
|
|
62
|
-
}, [code,
|
|
54
|
+
}, [code, onSignedIn, onDone]);
|
|
63
55
|
|
|
64
56
|
return (
|
|
65
57
|
<View style={styles.container}>
|
package/src/auth/LoginScreen.tsx
CHANGED
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* client ids are baked into the SDK.
|
|
9
9
|
*
|
|
10
10
|
* Host-injected seams:
|
|
11
|
-
* - activeTenant the reactive pre-auth
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
11
|
+
* - activeTenant the reactive pre-auth team (re-fetches the config);
|
|
12
|
+
* '' → the screen asks for the team
|
|
13
|
+
* - onPickTenant the user named a team ("Use a different team", or no
|
|
14
|
+
* team yet): the host stores it as its signed-out pick
|
|
15
|
+
* - onConsumerDisabled the team has no consumer sign-in: the host clears its
|
|
16
|
+
* pick; the screen asks for another team
|
|
14
17
|
* - onSignedIn session adoption (host: authStore.login)
|
|
15
18
|
* - onJoinTeam? open the host's "join a team by name" (OpenJoin) sub-view
|
|
16
19
|
*
|
|
@@ -19,20 +22,21 @@
|
|
|
19
22
|
* to signInWithGoogle. The host wraps this in its own SafeAreaView.
|
|
20
23
|
*/
|
|
21
24
|
import { useEffect, useMemo, useState } from 'react';
|
|
22
|
-
import { View, Text, Pressable, StyleSheet, Platform, ActivityIndicator } from 'react-native';
|
|
25
|
+
import { View, Text, Pressable, StyleSheet, Platform, ActivityIndicator, TextInput } from 'react-native';
|
|
23
26
|
import { fetchConsumerConfig, signInWithGoogle, signInWithApple, ada, type ConsumerCapabilityConfig } from '@ateam-ai-mobile/core';
|
|
24
27
|
import { AdaOrb } from '@ateam-ai-mobile/plugin-runtime';
|
|
25
28
|
import { getOAuthProvider } from '../providers';
|
|
26
|
-
import { googleClientIds,
|
|
29
|
+
import { googleClientIds, isConsumerDisabled, completeProviderSignIn } from './login';
|
|
27
30
|
|
|
28
31
|
const fontFam = Platform.OS === 'ios' ? 'SF Pro Rounded' : undefined;
|
|
29
32
|
|
|
30
33
|
export interface LoginScreenProps {
|
|
31
|
-
/** The reactive pre-auth
|
|
34
|
+
/** The reactive pre-auth team (host: useSession().tenant). Re-fetches config on change.
|
|
35
|
+
* '' → the screen asks for the team. */
|
|
32
36
|
activeTenant: string;
|
|
33
|
-
/** The
|
|
34
|
-
|
|
35
|
-
/**
|
|
37
|
+
/** The user named a team to sign in to: the host stores it as its signed-out pick. */
|
|
38
|
+
onPickTenant: (tenant: string) => void | Promise<void>;
|
|
39
|
+
/** The team has no consumer sign-in: the host clears its pick (the screen asks for another). */
|
|
36
40
|
onConsumerDisabled: () => void | Promise<void>;
|
|
37
41
|
/** Adopt the session JWT (host: authStore.login → core session + tenantConfirmed). */
|
|
38
42
|
onSignedIn: (jwt: string) => void | Promise<void>;
|
|
@@ -40,27 +44,37 @@ export interface LoginScreenProps {
|
|
|
40
44
|
onJoinTeam?: () => void;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
export default function LoginScreen({ activeTenant,
|
|
47
|
+
export default function LoginScreen({ activeTenant, onPickTenant, onConsumerDisabled, onSignedIn, onJoinTeam }: LoginScreenProps) {
|
|
44
48
|
const [config, setConfig] = useState<ConsumerCapabilityConfig | null>(null);
|
|
45
49
|
const [error, setError] = useState<string | null>(null);
|
|
50
|
+
// The team whose config said "no consumer sign-in", kept across the host
|
|
51
|
+
// clearing its pick so the ask-for-team view can say why it is back.
|
|
52
|
+
const [disabledTeam, setDisabledTeam] = useState<string | null>(null);
|
|
53
|
+
// "Use a different team" — or the current team can't sign in.
|
|
54
|
+
const [askTeam, setAskTeam] = useState(false);
|
|
55
|
+
const askingTeam = !activeTenant || askTeam;
|
|
56
|
+
const pick = async (tenant: string) => { setAskTeam(false); setDisabledTeam(null); await onPickTenant(tenant); };
|
|
46
57
|
|
|
47
58
|
useEffect(() => {
|
|
48
59
|
setConfig(null);
|
|
49
60
|
setError(null);
|
|
61
|
+
if (!activeTenant) return;
|
|
50
62
|
fetchConsumerConfig()
|
|
51
63
|
.then(setConfig)
|
|
52
64
|
.catch(async err => {
|
|
53
65
|
const msg = err?.message || 'Failed to load sign-in config';
|
|
54
66
|
// A consumer-disabled tenant 404s with "capability not enabled". Don't
|
|
55
|
-
// dead-end on red text —
|
|
56
|
-
//
|
|
57
|
-
if (
|
|
67
|
+
// dead-end on red text — ask for another team (and the host clears its
|
|
68
|
+
// pick). Asking here, not re-fetching, so a disabled fallback can't loop.
|
|
69
|
+
if (isConsumerDisabled(msg)) {
|
|
70
|
+
setDisabledTeam(activeTenant);
|
|
71
|
+
setAskTeam(true);
|
|
58
72
|
await onConsumerDisabled();
|
|
59
73
|
return;
|
|
60
74
|
}
|
|
61
75
|
setError(msg);
|
|
62
76
|
});
|
|
63
|
-
}, [activeTenant,
|
|
77
|
+
}, [activeTenant, onConsumerDisabled]);
|
|
64
78
|
|
|
65
79
|
return (
|
|
66
80
|
<View style={styles.container}>
|
|
@@ -71,9 +85,15 @@ export default function LoginScreen({ activeTenant, defaultTenant, onConsumerDis
|
|
|
71
85
|
</View>
|
|
72
86
|
|
|
73
87
|
<View style={styles.buttonSection}>
|
|
74
|
-
{
|
|
75
|
-
{
|
|
76
|
-
{!!
|
|
88
|
+
{askingTeam && <TeamField notice={disabledTeam} onPick={pick} />}
|
|
89
|
+
{!askingTeam && !config && !error && <ActivityIndicator color={ada.ink} />}
|
|
90
|
+
{!askingTeam && !!error && <Text style={styles.errorText}>{error}</Text>}
|
|
91
|
+
{!askingTeam && !!config && <SignInButtons config={config} onSignedIn={onSignedIn} />}
|
|
92
|
+
{!askingTeam && (
|
|
93
|
+
<Pressable onPress={() => setAskTeam(true)} hitSlop={8} style={{ paddingVertical: 4 }}>
|
|
94
|
+
<Text style={styles.joinLink}>Use a different team</Text>
|
|
95
|
+
</Pressable>
|
|
96
|
+
)}
|
|
77
97
|
{onJoinTeam && (
|
|
78
98
|
<Pressable onPress={onJoinTeam} hitSlop={8} style={{ paddingVertical: 8 }}>
|
|
79
99
|
<Text style={styles.joinLink}>Join a team by name</Text>
|
|
@@ -85,6 +105,33 @@ export default function LoginScreen({ activeTenant, defaultTenant, onConsumerDis
|
|
|
85
105
|
);
|
|
86
106
|
}
|
|
87
107
|
|
|
108
|
+
/** Ask which team to sign in to. Signing in needs the team's own consumer
|
|
109
|
+
* config (its client ids). */
|
|
110
|
+
function TeamField({ notice, onPick }: { notice: string | null; onPick: (tenant: string) => void | Promise<void> }) {
|
|
111
|
+
const [team, setTeam] = useState('');
|
|
112
|
+
const slug = team.trim().toLowerCase();
|
|
113
|
+
const submit = () => { if (slug) void onPick(slug); };
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
{!!notice && <Text style={styles.errorText}>“{notice}” doesn't have app sign-in. Try another team.</Text>}
|
|
117
|
+
<TextInput
|
|
118
|
+
style={styles.teamInput}
|
|
119
|
+
value={team}
|
|
120
|
+
onChangeText={setTeam}
|
|
121
|
+
placeholder="your team (e.g. acme)"
|
|
122
|
+
placeholderTextColor={ada.inkMuted}
|
|
123
|
+
autoCapitalize="none"
|
|
124
|
+
autoCorrect={false}
|
|
125
|
+
returnKeyType="go"
|
|
126
|
+
onSubmitEditing={submit}
|
|
127
|
+
/>
|
|
128
|
+
<Pressable style={[styles.signInBtn, !slug && styles.signInBtnDisabled]} onPress={submit} disabled={!slug}>
|
|
129
|
+
<Text style={styles.signInText}>Continue</Text>
|
|
130
|
+
</Pressable>
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
88
135
|
function SignInButtons({ config, onSignedIn }: { config: ConsumerCapabilityConfig; onSignedIn: (jwt: string) => void | Promise<void> }) {
|
|
89
136
|
const [busy, setBusy] = useState(false);
|
|
90
137
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -175,6 +222,10 @@ const styles = StyleSheet.create({
|
|
|
175
222
|
signInGlyph: { fontSize: 18, fontWeight: '700', color: ada.paper, fontFamily: fontFam },
|
|
176
223
|
signInText: { fontSize: 16, fontWeight: '600', color: ada.paper, letterSpacing: -0.2, fontFamily: fontFam },
|
|
177
224
|
appleBtn: { width: '100%', height: 52 },
|
|
225
|
+
teamInput: {
|
|
226
|
+
width: '100%', backgroundColor: ada.paperSoft, borderWidth: 1, borderColor: ada.border, borderRadius: 14,
|
|
227
|
+
paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: ada.ink,
|
|
228
|
+
},
|
|
178
229
|
errorText: { fontSize: 13, color: '#c43d3d', textAlign: 'center', paddingHorizontal: 16 },
|
|
179
230
|
disclaimer: { fontSize: 12, color: ada.inkMuted, textAlign: 'center', marginTop: 8, lineHeight: 18, fontFamily: fontFam },
|
|
180
231
|
joinLink: { fontSize: 14, fontWeight: '600', color: ada.orangeDeep, textAlign: 'center', fontFamily: fontFam },
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Native id_token acquisition + the Apple HIG button come from the host-wired
|
|
10
10
|
* OAuthProvider (setOAuthProvider). With no cfg passed to signInWithGoogle, the
|
|
11
11
|
* provider uses the host's BUNDLED Google client id (this screen carries no
|
|
12
|
-
* tenant config). Everything host-specific — session, the
|
|
13
|
-
*
|
|
12
|
+
* tenant config). Everything host-specific — session, the back transition, the
|
|
13
|
+
* SafeAreaView — is injected. The joined tenant is the JWT's own claim.
|
|
14
14
|
*/
|
|
15
15
|
import { useEffect, useState } from 'react';
|
|
16
16
|
import { View, Text, Pressable, StyleSheet, ActivityIndicator, Platform, TextInput } from 'react-native';
|
|
@@ -22,15 +22,13 @@ import { completeOpenJoin, openJoinErrorMessage } from './openJoin';
|
|
|
22
22
|
const fontFam = Platform.OS === 'ios' ? 'SF Pro Rounded' : undefined;
|
|
23
23
|
|
|
24
24
|
export interface OpenJoinScreenProps {
|
|
25
|
-
/** Adopt the joined tenant (host's single ACTIVE_TENANT writer), awaited before sign-in. */
|
|
26
|
-
onAdoptTenant: (tenant: string) => void | Promise<void>;
|
|
27
25
|
/** Adopt the session JWT (host: authStore.login → core session + tenantConfirmed). */
|
|
28
26
|
onSignedIn: (jwt: string) => void | Promise<void>;
|
|
29
27
|
/** Back to sign in — the host toggles the pre-auth phase (not a router). */
|
|
30
28
|
onBack: () => void;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
export default function OpenJoinScreen({
|
|
31
|
+
export default function OpenJoinScreen({ onSignedIn, onBack }: OpenJoinScreenProps) {
|
|
34
32
|
const [team, setTeam] = useState('');
|
|
35
33
|
const [busy, setBusy] = useState(false);
|
|
36
34
|
const [err, setErr] = useState<string | null>(null);
|
|
@@ -54,7 +52,7 @@ export default function OpenJoinScreen({ onAdoptTenant, onSignedIn, onBack }: Op
|
|
|
54
52
|
setErr(null); setBusy(true);
|
|
55
53
|
try {
|
|
56
54
|
const idToken = await acquire(oauth);
|
|
57
|
-
await completeOpenJoin(slug, idToken, { join,
|
|
55
|
+
await completeOpenJoin(slug, idToken, { join, onSignedIn });
|
|
58
56
|
} catch (e: any) {
|
|
59
57
|
setErr(openJoinErrorMessage(e?.message));
|
|
60
58
|
} finally {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Handoff exchange — the pure, testable half of HandoffScreen (R9B).
|
|
3
3
|
*
|
|
4
4
|
* Kept JSX-free in its own module (like R6's runLogoutTeardown) so the
|
|
5
|
-
* security-sensitive error mapper and the
|
|
5
|
+
* security-sensitive error mapper and the sign-in-before-clear ORDERING can be
|
|
6
6
|
* node-tested directly, without rendering the RN component.
|
|
7
7
|
*/
|
|
8
8
|
import type { HandoffExchangeResult } from '@ateam-ai-mobile/core';
|
|
@@ -10,8 +10,6 @@ import type { HandoffExchangeResult } from '@ateam-ai-mobile/core';
|
|
|
10
10
|
export interface HandoffExchangeDeps {
|
|
11
11
|
/** Trade the one-time code for a session JWT (host injects core.exchangeHandoff). */
|
|
12
12
|
exchange: (code: string) => Promise<HandoffExchangeResult>;
|
|
13
|
-
/** Adopt the handed-off tenant (host's single tenant writer). */
|
|
14
|
-
onAdoptTenant: (tenant: string) => void | Promise<void>;
|
|
15
13
|
/** Adopt the session JWT. */
|
|
16
14
|
onSignedIn: (jwt: string) => void | Promise<void>;
|
|
17
15
|
/** Clear the consumed single-use code. */
|
|
@@ -19,15 +17,14 @@ export interface HandoffExchangeDeps {
|
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
/**
|
|
22
|
-
* Run the handoff: exchange →
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* Run the handoff: exchange → sign in → clear the code, in that exact order. The
|
|
21
|
+
* handed-off tenant is the JWT's own `tenant` claim (getTenant() reads it), so a
|
|
22
|
+
* cross-tenant handoff is scoped correctly with nothing for the host to store.
|
|
23
|
+
* Throws on any failure (the caller maps it to a user-facing message);
|
|
24
|
+
* onSignedIn / onDone never run on the error path.
|
|
27
25
|
*/
|
|
28
26
|
export async function runHandoffExchange(code: string, deps: HandoffExchangeDeps): Promise<void> {
|
|
29
27
|
const res = await deps.exchange(code);
|
|
30
|
-
if (res.tenant) await deps.onAdoptTenant(res.tenant);
|
|
31
28
|
await deps.onSignedIn(res.jwt);
|
|
32
29
|
deps.onDone();
|
|
33
30
|
}
|
package/src/auth/login.ts
CHANGED
|
@@ -21,17 +21,12 @@ export function googleClientIds(config: ConsumerCapabilityConfig): { webClientId
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Whether a failed config fetch
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* Guarded by currentTenant !== defaultTenant so we never loop on the default.
|
|
24
|
+
* Whether a failed config fetch means the picked tenant has no consumer sign-in
|
|
25
|
+
* ("capability not enabled" 404). The host then clears its pick and shows its
|
|
26
|
+
* team picker — it never falls back to a default tenant.
|
|
28
27
|
*/
|
|
29
|
-
export function
|
|
30
|
-
errorMsg
|
|
31
|
-
currentTenant: string,
|
|
32
|
-
defaultTenant: string,
|
|
33
|
-
): boolean {
|
|
34
|
-
return /capability not enabled/i.test(errorMsg || '') && currentTenant !== defaultTenant;
|
|
28
|
+
export function isConsumerDisabled(errorMsg: string | undefined): boolean {
|
|
29
|
+
return /capability not enabled/i.test(errorMsg || '');
|
|
35
30
|
}
|
|
36
31
|
|
|
37
32
|
export interface ProviderSignInDeps {
|
package/src/auth/openJoin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Open self-signup — the pure, testable half of OpenJoinScreen (R9B).
|
|
3
3
|
*
|
|
4
|
-
* JSX-free so the join ORDERING (join →
|
|
4
|
+
* JSX-free so the join ORDERING (join → sign in) and the error
|
|
5
5
|
* mapper are node-testable without rendering. The native id_token acquisition
|
|
6
6
|
* happens in the component via getOAuthProvider(); the acquired token is passed
|
|
7
7
|
* in here, so this module stays pure and provider-free.
|
|
@@ -11,8 +11,6 @@ import type { ConsumerSignInResult } from '@ateam-ai-mobile/core';
|
|
|
11
11
|
export interface OpenJoinDeps {
|
|
12
12
|
/** Exchange { tenant, id_token } for a session JWT (host injects core.openJoinWith*). */
|
|
13
13
|
join: (slug: string, idToken: string) => Promise<ConsumerSignInResult>;
|
|
14
|
-
/** Adopt the joined tenant (host's single ACTIVE_TENANT writer). */
|
|
15
|
-
onAdoptTenant: (tenant: string) => void | Promise<void>;
|
|
16
14
|
/** Adopt the session JWT. */
|
|
17
15
|
onSignedIn: (jwt: string) => void | Promise<void>;
|
|
18
16
|
}
|
|
@@ -22,10 +20,10 @@ export interface OpenJoinDeps {
|
|
|
22
20
|
*
|
|
23
21
|
* Returns false WITHOUT side effects when idToken is null — the caller acquired
|
|
24
22
|
* no token (the user cancelled the native sheet, or it returned none), which is
|
|
25
|
-
* a silent no-op, never an error. Otherwise: join →
|
|
26
|
-
*
|
|
27
|
-
* the
|
|
28
|
-
* a
|
|
23
|
+
* a silent no-op, never an error. Otherwise: join → sign in. The joined tenant
|
|
24
|
+
* is the returned JWT's own `tenant` claim (getTenant() reads it), so there is
|
|
25
|
+
* nothing for the host to store. Throws on a join failure (the caller maps it to
|
|
26
|
+
* a message); sign-in never runs then.
|
|
29
27
|
*/
|
|
30
28
|
export async function completeOpenJoin(
|
|
31
29
|
slug: string,
|
|
@@ -34,7 +32,6 @@ export async function completeOpenJoin(
|
|
|
34
32
|
): Promise<boolean> {
|
|
35
33
|
if (!idToken) return false;
|
|
36
34
|
const consumer = await deps.join(slug, idToken);
|
|
37
|
-
await deps.onAdoptTenant(slug);
|
|
38
35
|
await deps.onSignedIn(consumer.jwt);
|
|
39
36
|
return true;
|
|
40
37
|
}
|
package/src/index.ts
CHANGED
|
@@ -60,7 +60,7 @@ export { completeInviteJoin, inviteHeadline } from './auth/inviteJoin';
|
|
|
60
60
|
export type { InviteJoinDeps } from './auth/inviteJoin';
|
|
61
61
|
export { default as LoginScreen } from './auth/LoginScreen';
|
|
62
62
|
export type { LoginScreenProps } from './auth/LoginScreen';
|
|
63
|
-
export { googleClientIds,
|
|
63
|
+
export { googleClientIds, isConsumerDisabled, completeProviderSignIn } from './auth/login';
|
|
64
64
|
export type { ProviderSignInDeps } from './auth/login';
|
|
65
65
|
|
|
66
66
|
// Voice (requires `voiceUrl` in the initAdas() config)
|
|
@@ -80,10 +80,12 @@ export {
|
|
|
80
80
|
setUIActionNavigator,
|
|
81
81
|
setPluginWebViewExtraHosts,
|
|
82
82
|
setOAuthProvider,
|
|
83
|
+
setCallAudioProvider,
|
|
83
84
|
} from './providers';
|
|
84
85
|
export type {
|
|
85
86
|
ClipboardProvider,
|
|
86
87
|
UIActionRoute,
|
|
87
88
|
OAuthProvider,
|
|
88
89
|
OAuthGoogleConfig,
|
|
90
|
+
CallAudioProvider,
|
|
89
91
|
} from './providers';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useCallback } from 'react';
|
|
1
|
+
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
2
2
|
import { View, Text, ActivityIndicator, StyleSheet, Platform } from 'react-native';
|
|
3
3
|
import { WebView } from 'react-native-webview';
|
|
4
4
|
import { getConfig, getCoreApiUrl, getVoiceApiUrl, getTenant, getToken, ada } from '@ateam-ai-mobile/core';
|
|
@@ -28,6 +28,9 @@ export default function EmbeddedPluginWebView({ pluginId, manifest, commandBuffe
|
|
|
28
28
|
const { webViewRef, sendInit, handleMessage } = usePluginBridge(pluginId, manifest._connectorId, commandBufferRef);
|
|
29
29
|
|
|
30
30
|
const url = resolvePluginUrl(manifest, tenant || undefined);
|
|
31
|
+
// An error belongs to the URL that produced it: a new URL (a fresh signature,
|
|
32
|
+
// a corrected tenant) gets a fresh load instead of the old error forever.
|
|
33
|
+
useEffect(() => { setError(null); }, [url]);
|
|
31
34
|
const bridgeJS = buildPluginBridgeJS(token || '', tenant || '');
|
|
32
35
|
|
|
33
36
|
// Hosts the plugin may navigate to — derived from the SDK config plus any
|
package/src/providers.ts
CHANGED
|
@@ -48,6 +48,28 @@ export function getOAuthProvider(): OAuthProvider | null {
|
|
|
48
48
|
return _oauth;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// ── Call audio mode ────────────────────────────────────────────────────────
|
|
52
|
+
// A voice call needs the device in its CALL audio mode, or the phone's mic
|
|
53
|
+
// hears the loudspeaker and the AI interrupts itself (iOS: playAndRecord +
|
|
54
|
+
// voiceChat). Native, so the host wires it (via @ateam-ai-mobile/expo);
|
|
55
|
+
// voiceSession calls beginCall when a call starts and endCall when it ends.
|
|
56
|
+
|
|
57
|
+
export interface CallAudioProvider {
|
|
58
|
+
beginCall(): void;
|
|
59
|
+
endCall(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let _callAudio: CallAudioProvider | null = null;
|
|
63
|
+
|
|
64
|
+
/** Register the device's call audio mode (e.g. via @ateam-ai-mobile/expo). */
|
|
65
|
+
export function setCallAudioProvider(callAudio: CallAudioProvider | null): void {
|
|
66
|
+
_callAudio = callAudio;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getCallAudioProvider(): CallAudioProvider | null {
|
|
70
|
+
return _callAudio;
|
|
71
|
+
}
|
|
72
|
+
|
|
51
73
|
// ── Clipboard ──────────────────────────────────────────────────────────────
|
|
52
74
|
|
|
53
75
|
export interface ClipboardProvider {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* cannot reach the WebView beyond that one injected call.
|
|
25
25
|
*/
|
|
26
26
|
import { getVoiceApiUrl, getToken, getTenant, getUser, refreshToken } from '@ateam-ai-mobile/core';
|
|
27
|
+
import { getCallAudioProvider } from '../providers';
|
|
27
28
|
import {
|
|
28
29
|
callDeviceTool,
|
|
29
30
|
ensureDeviceToolsLoaded,
|
|
@@ -284,6 +285,11 @@ interface InternalState {
|
|
|
284
285
|
persona?: VoicePersona;
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
/** A call that holds the mic/speaker: connecting or connected. */
|
|
289
|
+
function isLiveCall(state: VoiceState): boolean {
|
|
290
|
+
return state === 'connecting' || state === 'connected';
|
|
291
|
+
}
|
|
292
|
+
|
|
287
293
|
const IDLE: InternalState = {
|
|
288
294
|
callState: 'idle',
|
|
289
295
|
sessionId: null,
|
|
@@ -833,10 +839,26 @@ class VoiceSessionManager implements VoiceSessionProvider {
|
|
|
833
839
|
// ── internals ────────────────────────────────────────────────
|
|
834
840
|
|
|
835
841
|
private setState(partial: Partial<InternalState>): void {
|
|
842
|
+
const wasLive = isLiveCall(this.state.callState);
|
|
836
843
|
this.state = { ...this.state, ...partial };
|
|
844
|
+
const isLive = isLiveCall(this.state.callState);
|
|
845
|
+
// The device's CALL audio mode follows the call: on as it starts connecting
|
|
846
|
+
// (before the WebView opens the mic), off as soon as it is no longer live —
|
|
847
|
+
// whichever path ends it (stop(), the page hanging up, an error).
|
|
848
|
+
if (wasLive !== isLive) this.setCallAudio(isLive);
|
|
837
849
|
this.emit({ type: 'state' });
|
|
838
850
|
}
|
|
839
851
|
|
|
852
|
+
private setCallAudio(on: boolean): void {
|
|
853
|
+
const audio = getCallAudioProvider();
|
|
854
|
+
if (!audio) return;
|
|
855
|
+
try {
|
|
856
|
+
if (on) audio.beginCall(); else audio.endCall();
|
|
857
|
+
} catch (e: any) {
|
|
858
|
+
console.warn('[voiceSession] call audio mode failed:', e?.message || String(e));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
840
862
|
private appendTranscript(entry: TranscriptEntry): void {
|
|
841
863
|
this.state = { ...this.state, transcript: [...this.state.transcript, entry] };
|
|
842
864
|
this.emit({ type: 'transcript' });
|