diff --git a/.gitignore b/.gitignore index 67f3212..46459dd 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,8 @@ android/generated # React Native Nitro Modules nitrogen/ + +# env files +.env +.env.local +!.env.example diff --git a/android/build.gradle b/android/build.gradle index 3de3ab8..db2bdb0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -64,7 +64,7 @@ android { dependencies { implementation "com.facebook.react:react-android" - implementation "androidx.credentials:credentials:1.7.0-alpha01" - implementation "androidx.credentials:credentials-play-services-auth:17.0-alpha01" - implementation "com.google.android.libraries.identity.googleid:googleid:1.2.0" + implementation "androidx.credentials:credentials:1.5.0" + implementation "androidx.credentials:credentials-play-services-auth:1.5.0" + implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1" } diff --git a/android/src/main/java/com/thoughtbot/reactnativesocialauth/GoogleSignInModule.kt b/android/src/main/java/com/thoughtbot/reactnativesocialauth/GoogleSignInModule.kt index a272cfa..fb55814 100644 --- a/android/src/main/java/com/thoughtbot/reactnativesocialauth/GoogleSignInModule.kt +++ b/android/src/main/java/com/thoughtbot/reactnativesocialauth/GoogleSignInModule.kt @@ -1,34 +1,185 @@ package com.thoughtbot.reactnativesocialauth +import androidx.credentials.ClearCredentialStateRequest +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.exceptions.GetCredentialCancellationException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.NoCredentialException +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap +import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch class GoogleSignInModule(reactContext: ReactApplicationContext) : NativeGoogleSignInSpec(reactContext) { + private val credentialManager: CredentialManager = + CredentialManager.create(reactContext) + + private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + private var webClientId: String? = null + private var nonce: String? = null + private var autoSelect: Boolean = false + private var currentUser: GoogleIdTokenCredential? = null + override fun configure(config: ReadableMap) { - // TODO: Phase 3 — Store configuration for Credential Manager + webClientId = config.getString("webClientId") + nonce = if (config.hasKey("nonce")) config.getString("nonce") else null + autoSelect = if (config.hasKey("autoSelect")) config.getBoolean("autoSelect") else false } override fun signIn(promise: Promise) { - promise.reject("ERR_NOT_IMPLEMENTED", "Google Sign-In is not yet implemented on Android") + val clientId = webClientId + if (clientId == null) { + promise.reject("ERR_NOT_CONFIGURED", "GoogleSignIn.configure() must be called before signIn()") + return + } + + val activity = currentActivity + if (activity == null) { + promise.reject("ERR_NO_ACTIVITY", "No current activity available") + return + } + + scope.launch { + try { + val result = getCredentialWithAutoSignIn(clientId, activity) + handleSignInResult(result, promise) + } catch (e: GetCredentialCancellationException) { + promise.reject("SIGN_IN_CANCELLED", "User cancelled the sign-in flow", e) + } catch (e: NoCredentialException) { + promise.reject("NO_CREDENTIALS", "No credentials available on this device", e) + } catch (e: GetCredentialException) { + promise.reject("SIGN_IN_FAILED", e.message, e) + } + } + } + + private suspend fun getCredentialWithAutoSignIn( + clientId: String, + activity: android.app.Activity + ): GetCredentialResponse { + val autoSignInOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(true) + .setServerClientId(clientId) + .setAutoSelectEnabled(true) + .apply { nonce?.let { setNonce(it) } } + .build() + + val autoRequest = GetCredentialRequest.Builder() + .addCredentialOption(autoSignInOption) + .build() + + return try { + credentialManager.getCredential(activity, autoRequest) + } catch (e: NoCredentialException) { + val fallbackOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(false) + .setServerClientId(clientId) + .setAutoSelectEnabled(false) + .apply { nonce?.let { setNonce(it) } } + .build() + + val fallbackRequest = GetCredentialRequest.Builder() + .addCredentialOption(fallbackOption) + .build() + + credentialManager.getCredential(activity, fallbackRequest) + } + } + + private fun handleSignInResult(result: GetCredentialResponse, promise: Promise) { + val credential = result.credential + + if (credential !is CustomCredential || + credential.type != GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + promise.reject("SIGN_IN_FAILED", "Unexpected credential type: ${credential.type}") + return + } + + val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) + currentUser = googleIdTokenCredential + + val userMap = Arguments.createMap().apply { + putString("id", googleIdTokenCredential.id) + putString("email", googleIdTokenCredential.id) + putString("displayName", googleIdTokenCredential.displayName) + putString("givenName", googleIdTokenCredential.givenName) + putString("familyName", googleIdTokenCredential.familyName) + putString("photoUrl", googleIdTokenCredential.profilePictureUri?.toString()) + } + + val resultMap = Arguments.createMap().apply { + putString("idToken", googleIdTokenCredential.idToken) + putNull("accessToken") + putNull("serverAuthCode") + putMap("user", userMap) + } + + promise.resolve(resultMap) } override fun signOut(promise: Promise) { - promise.reject("ERR_NOT_IMPLEMENTED", "Google Sign-In is not yet implemented on Android") + scope.launch { + try { + credentialManager.clearCredentialState(ClearCredentialStateRequest()) + currentUser = null + promise.resolve(null) + } catch (e: Exception) { + promise.reject("SIGN_OUT_FAILED", e.message, e) + } + } } override fun getCurrentUser(promise: Promise) { - promise.reject("ERR_NOT_IMPLEMENTED", "Google Sign-In is not yet implemented on Android") + val user = currentUser + if (user == null) { + promise.resolve(null) + return + } + + val userMap = Arguments.createMap().apply { + putString("id", user.id) + putString("email", user.id) + putString("displayName", user.displayName) + putString("givenName", user.givenName) + putString("familyName", user.familyName) + putString("photoUrl", user.profilePictureUri?.toString()) + } + + promise.resolve(userMap) } override fun revokeAccess(promise: Promise) { - promise.reject("ERR_NOT_IMPLEMENTED", "Google Sign-In is not yet implemented on Android") + scope.launch { + try { + credentialManager.clearCredentialState(ClearCredentialStateRequest()) + currentUser = null + promise.resolve(null) + } catch (e: Exception) { + promise.reject("REVOKE_FAILED", e.message, e) + } + } } override fun isSignedIn(): Boolean { - return false + return currentUser != null + } + + override fun invalidate() { + scope.cancel() + super.invalidate() } companion object { diff --git a/example/.env.example b/example/.env.example new file mode 100644 index 0000000..1dd9da6 --- /dev/null +++ b/example/.env.example @@ -0,0 +1,3 @@ +# Google OAuth Web Client ID from Google Cloud Console +# https://console.cloud.google.com/apis/credentials +EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=your-web-client-id-here.apps.googleusercontent.com diff --git a/example/package.json b/example/package.json index 6af16d9..91d0168 100644 --- a/example/package.json +++ b/example/package.json @@ -18,6 +18,7 @@ "react": "19.2.0", "react-dom": "19.2.0", "react-native": "0.83.4", + "react-native-svg": "^15.15.5", "react-native-web": "~0.21.0" }, "private": true, diff --git a/example/src/App.tsx b/example/src/App.tsx index 5074310..10ceb89 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,39 +1,237 @@ -import { Text, View, StyleSheet } from 'react-native'; +import { useState } from 'react'; +import { + Text, + View, + StyleSheet, + ScrollView, + Alert, + SafeAreaView, +} from 'react-native'; import { GoogleSignIn, GoogleSignInButton, + isGoogleSignInError, + GoogleSignInErrorCode, + type GoogleUser, } from '@thoughtbot/react-native-social-auth'; +const WEB_CLIENT_ID = process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID ?? ''; + export default function App() { + const [user, setUser] = useState(null); + const handleSignIn = async () => { + if (!WEB_CLIENT_ID) { + Alert.alert( + 'Missing config', + 'Set EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID in example/.env' + ); + return; + } try { - GoogleSignIn.configure({ - webClientId: 'YOUR_WEB_CLIENT_ID', - }); + GoogleSignIn.configure({ webClientId: WEB_CLIENT_ID }); const credential = await GoogleSignIn.signIn(); - console.log('Signed in:', credential.user.email); + setUser(credential.user); } catch (error) { - console.error('Sign in failed:', error); + if (isGoogleSignInError(error)) { + switch (error.code) { + case GoogleSignInErrorCode.SIGN_IN_CANCELLED: + break; + case GoogleSignInErrorCode.NO_CREDENTIALS: + Alert.alert( + 'No Credentials', + 'No Google accounts found on this device.' + ); + break; + case GoogleSignInErrorCode.PLAY_SERVICES_NOT_AVAILABLE: + Alert.alert('Error', 'Google Play Services is not available.'); + break; + default: + Alert.alert('Sign In Failed', error.message); + } + } else { + Alert.alert('Error', 'An unexpected error occurred.'); + } + } + }; + + const handleSignOut = async () => { + try { + await GoogleSignIn.signOut(); + setUser(null); + } catch (e) { + console.error('Sign out error:', e); + Alert.alert('Error', 'Sign out failed.'); } }; + if (user) { + return ( + + + Welcome + {user.displayName} + {user.email} + + + + ); + } + return ( - - Social Auth Example - - + + + Social Auth Example + + Light Theme + + + + + + + + Dark Theme + + + + + + + + Neutral Theme + + + + + + + + Square Shape + + + + + + + + Disabled State + + + + + + + ); } const styles = StyleSheet.create({ container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + scroll: { + padding: 24, + gap: 16, + }, + heading: { + fontSize: 22, + fontWeight: '700', + color: '#1f1f1f', + marginBottom: 8, + }, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#666', + marginTop: 8, + }, + row: { + gap: 10, + }, + profile: { flex: 1, alignItems: 'center', justifyContent: 'center', - gap: 24, + gap: 8, }, - title: { + name: { fontSize: 18, fontWeight: '600', + color: '#1f1f1f', + }, + email: { + fontSize: 14, + color: '#666', + }, + signOutButton: { + marginTop: 16, }, }); diff --git a/example/src/env.d.ts b/example/src/env.d.ts new file mode 100644 index 0000000..68f4fd6 --- /dev/null +++ b/example/src/env.d.ts @@ -0,0 +1,5 @@ +declare const process: { + env: { + EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID?: string; + }; +}; diff --git a/package.json b/package.json index 8d73f7b..412450a 100644 --- a/package.json +++ b/package.json @@ -81,13 +81,15 @@ "react": "19.2.0", "react-native": "0.83.4", "react-native-builder-bob": "^0.41.0", + "react-native-svg": "^15.15.5", "release-it": "^19.2.4", "turbo": "^2.8.21", "typescript": "^6.0.2" }, "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-svg": ">=13.0.0" }, "workspaces": [ "example" diff --git a/src/google/GoogleLogo.tsx b/src/google/GoogleLogo.tsx new file mode 100644 index 0000000..9a3ed31 --- /dev/null +++ b/src/google/GoogleLogo.tsx @@ -0,0 +1,35 @@ +import Svg, { Path, ClipPath, Rect, Defs, G } from 'react-native-svg'; + +interface GoogleLogoProps { + size?: number; +} + +export function GoogleLogo({ size = 20 }: GoogleLogoProps) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/src/google/GoogleSignInButton.tsx b/src/google/GoogleSignInButton.tsx index 1a6fcf8..a955f0e 100644 --- a/src/google/GoogleSignInButton.tsx +++ b/src/google/GoogleSignInButton.tsx @@ -1,17 +1,25 @@ import { Pressable, + View, Text, StyleSheet, type StyleProp, type ViewStyle, } from 'react-native'; +import { GoogleLogo } from './GoogleLogo'; export type GoogleSignInButtonTheme = 'light' | 'dark' | 'neutral'; -export type GoogleSignInButtonSize = 'icon' | 'standard' | 'wide'; +export type GoogleSignInButtonShape = 'rounded' | 'square'; + +export type GoogleSignInButtonText = 'signin' | 'signup' | 'continue'; + +export type GoogleSignInButtonSize = 'standard' | 'icon'; export interface GoogleSignInButtonProps { theme?: GoogleSignInButtonTheme; + shape?: GoogleSignInButtonShape; + text?: GoogleSignInButtonText; size?: GoogleSignInButtonSize; onPress?: () => void; disabled?: boolean; @@ -19,34 +27,85 @@ export interface GoogleSignInButtonProps { testID?: string; } +const BUTTON_LABELS: Record = { + signin: 'Sign in with Google', + signup: 'Sign up with Google', + continue: 'Continue with Google', +}; + +const THEME_STYLES: Record< + GoogleSignInButtonTheme, + { + backgroundColor: string; + borderColor: string; + borderWidth: number; + textColor: string; + } +> = { + light: { + backgroundColor: '#FFFFFF', + borderColor: '#747775', + borderWidth: 1, + textColor: '#1F1F1F', + }, + dark: { + backgroundColor: '#131314', + borderColor: '#8E918F', + borderWidth: 1, + textColor: '#E3E3E3', + }, + neutral: { + backgroundColor: '#F2F2F2', + borderColor: 'transparent', + borderWidth: 0, + textColor: '#1F1F1F', + }, +}; + export function GoogleSignInButton({ theme = 'light', + shape = 'rounded', + text = 'signin', size = 'standard', onPress, disabled = false, style, testID, }: GoogleSignInButtonProps) { + const themeStyle = THEME_STYLES[theme]; + const isIcon = size === 'icon'; + const borderRadius = shape === 'rounded' ? 20 : 4; + const label = BUTTON_LABELS[text]; + return ( - {size !== 'icon' && ( - - Sign in with Google + + + + {!isIcon && ( + + {label} )} @@ -54,44 +113,33 @@ export function GoogleSignInButton({ } const styles = StyleSheet.create({ - base: { + button: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 16, - paddingVertical: 10, - borderRadius: 20, - backgroundColor: '#ffffff', - borderWidth: 1, - borderColor: '#dadce0', - minHeight: 40, - }, - dark: { - backgroundColor: '#131314', - borderColor: '#8e918f', + height: 40, + alignSelf: 'flex-start', }, - neutral: { - backgroundColor: '#f2f2f2', - borderColor: '#f2f2f2', - }, - wide: { - paddingHorizontal: 24, - minWidth: 280, + iconButton: { + width: 40, + justifyContent: 'center', }, - iconOnly: { - paddingHorizontal: 10, - minWidth: 40, + logoContainer: { + marginLeft: 12, + marginRight: 10, }, - disabled: { - opacity: 0.38, + iconLogoContainer: { + alignItems: 'center', + justifyContent: 'center', + flex: 1, }, - text: { + label: { + fontFamily: 'Roboto-Medium', fontSize: 14, + lineHeight: 20, fontWeight: '500', - color: '#1f1f1f', - letterSpacing: 0.25, + marginRight: 12, }, - textDark: { - color: '#e3e3e3', + disabled: { + opacity: 0.38, }, }); diff --git a/src/google/index.ts b/src/google/index.ts index ff4b7da..63fee7d 100644 --- a/src/google/index.ts +++ b/src/google/index.ts @@ -8,6 +8,8 @@ export { GoogleSignInButton } from './GoogleSignInButton'; export type { GoogleSignInButtonProps, GoogleSignInButtonTheme, + GoogleSignInButtonShape, + GoogleSignInButtonText, GoogleSignInButtonSize, } from './GoogleSignInButton'; export type { diff --git a/src/index.tsx b/src/index.tsx index 806ea3a..4394ed0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,6 +9,8 @@ export { export type { GoogleSignInButtonProps, GoogleSignInButtonTheme, + GoogleSignInButtonShape, + GoogleSignInButtonText, GoogleSignInButtonSize, GoogleSignInConfig, GoogleUser, diff --git a/yarn.lock b/yarn.lock index a2526c5..530b68c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3775,6 +3775,7 @@ __metadata: react-native: "npm:0.83.4" react-native-builder-bob: "npm:^0.41.0" react-native-monorepo-config: "npm:^0.3.3" + react-native-svg: "npm:^15.15.5" react-native-web: "npm:~0.21.0" languageName: unknown linkType: soft @@ -3805,12 +3806,14 @@ __metadata: react: "npm:19.2.0" react-native: "npm:0.83.4" react-native-builder-bob: "npm:^0.41.0" + react-native-svg: "npm:^15.15.5" release-it: "npm:^19.2.4" turbo: "npm:^2.8.21" typescript: "npm:^6.0.2" peerDependencies: react: "*" react-native: "*" + react-native-svg: ">=13.0.0" languageName: unknown linkType: soft @@ -4996,6 +4999,13 @@ __metadata: languageName: node linkType: hard +"boolbase@npm:^1.0.0": + version: 1.0.0 + resolution: "boolbase@npm:1.0.0" + checksum: 10c0/e4b53deb4f2b85c52be0e21a273f2045c7b6a6ea002b0e139c744cb6f95e9ec044439a52883b0d74dedd1ff3da55ed140cfdddfed7fb0cccbed373de5dce1bcf + languageName: node + linkType: hard + "bplist-creator@npm:0.1.0": version: 0.1.0 resolution: "bplist-creator@npm:0.1.0" @@ -5734,6 +5744,36 @@ __metadata: languageName: node linkType: hard +"css-select@npm:^5.1.0": + version: 5.2.2 + resolution: "css-select@npm:5.2.2" + dependencies: + boolbase: "npm:^1.0.0" + css-what: "npm:^6.1.0" + domhandler: "npm:^5.0.2" + domutils: "npm:^3.0.1" + nth-check: "npm:^2.0.1" + checksum: 10c0/d79fffa97106007f2802589f3ed17b8c903f1c961c0fc28aa8a051eee0cbad394d8446223862efd4c1b40445a6034f626bb639cf2035b0bfc468544177593c99 + languageName: node + linkType: hard + +"css-tree@npm:^1.1.3": + version: 1.1.3 + resolution: "css-tree@npm:1.1.3" + dependencies: + mdn-data: "npm:2.0.14" + source-map: "npm:^0.6.1" + checksum: 10c0/499a507bfa39b8b2128f49736882c0dd636b0cd3370f2c69f4558ec86d269113286b7df469afc955de6a68b0dba00bc533e40022a73698081d600072d5d83c1c + languageName: node + linkType: hard + +"css-what@npm:^6.1.0": + version: 6.2.2 + resolution: "css-what@npm:6.2.2" + checksum: 10c0/91e24c26fb977b4ccef30d7007d2668c1c10ac0154cc3f42f7304410e9594fb772aea4f30c832d2993b132ca8d99338050866476210316345ec2e7d47b248a56 + languageName: node + linkType: hard + "csstype@npm:^3.2.2": version: 3.2.3 resolution: "csstype@npm:3.2.3" @@ -5997,6 +6037,44 @@ __metadata: languageName: node linkType: hard +"dom-serializer@npm:^2.0.0": + version: 2.0.0 + resolution: "dom-serializer@npm:2.0.0" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.2" + entities: "npm:^4.2.0" + checksum: 10c0/d5ae2b7110ca3746b3643d3ef60ef823f5f078667baf530cec096433f1627ec4b6fa8c072f09d079d7cda915fd2c7bc1b7b935681e9b09e591e1e15f4040b8e2 + languageName: node + linkType: hard + +"domelementtype@npm:^2.3.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: 10c0/686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9 + languageName: node + linkType: hard + +"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": + version: 5.0.3 + resolution: "domhandler@npm:5.0.3" + dependencies: + domelementtype: "npm:^2.3.0" + checksum: 10c0/bba1e5932b3e196ad6862286d76adc89a0dbf0c773e5ced1eb01f9af930c50093a084eff14b8de5ea60b895c56a04d5de8bbc4930c5543d029091916770b2d2a + languageName: node + linkType: hard + +"domutils@npm:^3.0.1": + version: 3.2.2 + resolution: "domutils@npm:3.2.2" + dependencies: + dom-serializer: "npm:^2.0.0" + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + checksum: 10c0/47938f473b987ea71cd59e59626eb8666d3aa8feba5266e45527f3b636c7883cca7e582d901531961f742c519d7514636b7973353b648762b2e3bedbf235fada + languageName: node + linkType: hard + "dot-prop@npm:^5.1.0": version: 5.3.0 resolution: "dot-prop@npm:5.3.0" @@ -6096,6 +6174,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^4.2.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0, env-paths@npm:^2.2.1": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -9783,6 +9868,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.0.14": + version: 2.0.14 + resolution: "mdn-data@npm:2.0.14" + checksum: 10c0/67241f8708c1e665a061d2b042d2d243366e93e5bf1f917693007f6d55111588b952dcbfd3ea9c2d0969fb754aad81b30fdcfdcc24546495fc3b24336b28d4bd + languageName: node + linkType: hard + "memoize-one@npm:^5.0.0": version: 5.2.1 resolution: "memoize-one@npm:5.2.1" @@ -10702,6 +10794,15 @@ __metadata: languageName: node linkType: hard +"nth-check@npm:^2.0.1": + version: 2.1.1 + resolution: "nth-check@npm:2.1.1" + dependencies: + boolbase: "npm:^1.0.0" + checksum: 10c0/5fee7ff309727763689cfad844d979aedd2204a817fbaaf0e1603794a7c20db28548d7b024692f953557df6ce4a0ee4ae46cd8ebd9b36cfb300b9226b567c479 + languageName: node + linkType: hard + "nullthrows@npm:^1.1.1": version: 1.1.1 resolution: "nullthrows@npm:1.1.1" @@ -11581,6 +11682,19 @@ __metadata: languageName: node linkType: hard +"react-native-svg@npm:^15.15.5": + version: 15.15.5 + resolution: "react-native-svg@npm:15.15.5" + dependencies: + css-select: "npm:^5.1.0" + css-tree: "npm:^1.1.3" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/34b71b6c83d7235efbc4263abaffcbad780375f9e0fca95786e4f1a543a2b6d07e90c7df426f708e85be68331c412066cdc639f4a6ce82bed42bc728050d9402 + languageName: node + linkType: hard + "react-native-web@npm:~0.21.0": version: 0.21.2 resolution: "react-native-web@npm:0.21.2"