Skip to content

Commit eb5fb63

Browse files
committed
feat(expo): add Android bottom sheet presentation
1 parent 083d40b commit eb5fb63

8 files changed

Lines changed: 93 additions & 3 deletions

File tree

.changeset/soft-diamonds-slide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/expo': minor
3+
---
4+
5+
Add an Android `presentation` option to `AuthView`, including a native Material bottom sheet mode.

packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@ package expo.modules.clerk
22

33
import android.content.Context
44
import android.util.Log
5+
import androidx.compose.foundation.isSystemInDarkTheme
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.BoxWithConstraints
58
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.height
11+
import androidx.compose.material3.ExperimentalMaterial3Api
12+
import androidx.compose.material3.ModalBottomSheet
13+
import androidx.compose.material3.SheetValue
14+
import androidx.compose.material3.rememberModalBottomSheetState
615
import androidx.compose.runtime.Composable
716
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.graphics.Color
818
import androidx.compose.ui.unit.dp
919
import androidx.lifecycle.ViewModelStore
1020
import androidx.lifecycle.ViewModelStoreOwner
@@ -30,6 +40,7 @@ class ClerkAuthNativeView(context: Context, appContext: AppContext) : ClerkCompo
3040
var isDismissible: Boolean = true
3141
var logoMaxHeight: Float? = null
3242
var mode: String? = null
43+
var presentation: String = "inline"
3344

3445
private val onAuthEvent by EventDispatcher()
3546

@@ -61,9 +72,37 @@ class ClerkAuthNativeView(context: Context, appContext: AppContext) : ClerkCompo
6172
}
6273

6374
@Composable
75+
@OptIn(ExperimentalMaterial3Api::class)
6476
override fun Content() {
65-
debugLog(TAG, "setupView - mode: $mode, isDismissible: $isDismissible, activity: $activity")
77+
debugLog(
78+
TAG,
79+
"setupView - mode: $mode, presentation: $presentation, isDismissible: $isDismissible, activity: $activity",
80+
)
81+
82+
if (presentation != "bottomSheet") {
83+
ClerkAuthView()
84+
return
85+
}
86+
87+
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
88+
ModalBottomSheet(
89+
onDismissRequest = ::sendDismissEvent,
90+
sheetState = sheetState,
91+
containerColor = clerkBackgroundColor(),
92+
) {
93+
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
94+
val authHeight =
95+
if (sheetState.currentValue == SheetValue.Expanded) maxHeight else maxHeight / 2
96+
97+
Box(modifier = Modifier.fillMaxWidth().height(authHeight)) {
98+
ClerkAuthView()
99+
}
100+
}
101+
}
102+
}
66103

104+
@Composable
105+
private fun ClerkAuthView() {
67106
AuthView(
68107
modifier = Modifier.fillMaxSize(),
69108
clerkTheme = authTheme(),
@@ -76,6 +115,17 @@ class ClerkAuthNativeView(context: Context, appContext: AppContext) : ClerkCompo
76115
)
77116
}
78117

118+
@Composable
119+
private fun clerkBackgroundColor(): Color {
120+
val isDarkMode = isSystemInDarkTheme()
121+
val customTheme = Clerk.customTheme
122+
val modeColors = if (isDarkMode) customTheme?.darkColors else customTheme?.lightColors
123+
124+
return modeColors?.background
125+
?: customTheme?.colors?.background
126+
?: if (isDarkMode) Color(0xFF131316) else Color.White
127+
}
128+
79129
private fun authTheme(): ClerkTheme? {
80130
val maxHeight = logoMaxHeight ?: return Clerk.customTheme
81131
val theme = Clerk.customTheme ?: ClerkTheme()
@@ -119,6 +169,10 @@ class ClerkAuthViewModule : Module() {
119169
view.logoMaxHeight = logoMaxHeight
120170
}
121171

172+
Prop("presentation") { view: ClerkAuthNativeView, presentation: String? ->
173+
view.presentation = presentation ?: "inline"
174+
}
175+
122176
OnViewDidUpdateProps { view: ClerkAuthNativeView ->
123177
view.setupView()
124178
}

packages/expo/src/native/AuthView.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type ReactElement, useCallback } from 'react';
22
import type { NativeSyntheticEvent } from 'react-native';
3-
import { Text, View } from 'react-native';
3+
import { Platform, Text, View } from 'react-native';
44

55
import NativeClerkAuthView from '../specs/NativeClerkAuthView';
66
import { isNativeSupported } from '../utils/native-module';
@@ -39,6 +39,7 @@ type AuthNativeEvent = NativeSyntheticEvent<Readonly<{ type: string }>>;
3939
*/
4040
export function AuthView({
4141
mode = 'signInOrUp',
42+
presentation = 'inline',
4243
isDismissible = true,
4344
logoMaxHeight,
4445
onDismiss,
@@ -68,6 +69,7 @@ export function AuthView({
6869
<NativeClerkAuthView
6970
style={{ flex: 1 }}
7071
mode={mode}
72+
{...(Platform.OS === 'android' ? { presentation } : {})}
7173
isDismissible={isDismissible}
7274
logoMaxHeight={logoMaxHeight}
7375
onAuthEvent={handleAuthEvent}

packages/expo/src/native/AuthView.types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
*/
88
export type AuthViewMode = 'signIn' | 'signUp' | 'signInOrUp';
99

10+
/** Android presentation used by the native authentication view. */
11+
export type AuthViewPresentation = 'inline' | 'bottomSheet';
12+
1013
/**
1114
* Props for the AuthView component.
1215
*
@@ -26,6 +29,17 @@ export interface AuthViewProps {
2629
*/
2730
mode?: AuthViewMode;
2831

32+
/**
33+
* Android presentation for the native authentication view.
34+
*
35+
* `'inline'` preserves the standard behavior and fills the parent container.
36+
* `'bottomSheet'` presents the same Clerk flow in a Material bottom sheet.
37+
* This option is ignored on iOS, where the parent controls presentation.
38+
*
39+
* @default 'inline'
40+
*/
41+
presentation?: AuthViewPresentation;
42+
2943
/**
3044
* Whether the authentication view can be dismissed by the user.
3145
*

packages/expo/src/native/__tests__/AuthView.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ vi.mock('../../utils/native-module', () => {
2424

2525
vi.mock('react-native', () => {
2626
return {
27+
Platform: { OS: 'android' },
2728
Text: ({ children }: { children?: React.ReactNode }) => React.createElement('span', null, children),
2829
View: ({ children }: { children?: React.ReactNode }) => React.createElement('div', null, children),
2930
};
@@ -40,6 +41,18 @@ describe('AuthView', () => {
4041
expect(mocks.NativeClerkAuthView.mock.calls[0]?.[0]).toMatchObject({ logoMaxHeight: 64 });
4142
});
4243

44+
test('uses inline presentation by default', () => {
45+
render(<AuthView />);
46+
47+
expect(mocks.NativeClerkAuthView.mock.calls[0]?.[0]).toMatchObject({ presentation: 'inline' });
48+
});
49+
50+
test('passes bottom sheet presentation to the native auth view', () => {
51+
render(<AuthView presentation='bottomSheet' />);
52+
53+
expect(mocks.NativeClerkAuthView.mock.calls[0]?.[0]).toMatchObject({ presentation: 'bottomSheet' });
54+
});
55+
4356
test('calls onDismiss when the native auth view emits dismissed', () => {
4457
const onDismiss = vi.fn();
4558

packages/expo/src/native/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*/
3030

3131
export { AuthView } from './AuthView';
32-
export type { AuthViewProps, AuthViewMode } from './AuthView.types';
32+
export type { AuthViewProps, AuthViewMode, AuthViewPresentation } from './AuthView.types';
3333
export { UserButton } from './UserButton';
3434
export { UserProfileView } from './UserProfileView';
3535
export type { UserProfileViewProps } from './UserProfileView';

packages/expo/src/specs/NativeClerkAuthView.android.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface NativeProps extends ViewProps {
77
mode?: string;
88
isDismissible?: boolean;
99
logoMaxHeight?: number;
10+
presentation?: string;
1011
onAuthEvent?: (event: NativeSyntheticEvent<AuthEvent>) => void;
1112
}
1213

packages/expo/src/specs/NativeClerkAuthView.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface NativeProps extends ViewProps {
88
mode?: string;
99
isDismissible?: boolean;
1010
logoMaxHeight?: number;
11+
presentation?: string;
1112
onAuthEvent?: (event: NativeSyntheticEvent<AuthEvent>) => void;
1213
}
1314

0 commit comments

Comments
 (0)