mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
@@ -36,3 +36,7 @@ IOS_PROVISIONING_PROFILE_NAME="provisioning profile name"
|
||||
IOS_SHARE_BUNDLE_ID="share bundle ID"
|
||||
IOS_SHARE_PROVISIONING_PROFILE_NAME="share provisioning profile name"
|
||||
SLACK_URL="Slack webhook URL"
|
||||
|
||||
# Third-party Sign in
|
||||
GOOGLE_WEB_CLIENT_ID=your-google-web-client-id
|
||||
GOOGLE_IOS_CLIENT_ID=your-google-ios-client-id
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
PODS:
|
||||
- AppAuth (1.7.6):
|
||||
- AppAuth/Core (= 1.7.6)
|
||||
- AppAuth/ExternalUserAgent (= 1.7.6)
|
||||
- AppAuth/Core (1.7.6)
|
||||
- AppAuth/ExternalUserAgent (1.7.6):
|
||||
- AppAuth/Core
|
||||
- boost (1.83.0)
|
||||
- BVLinearGradient (2.8.3):
|
||||
- React-Core
|
||||
@@ -21,6 +27,14 @@ PODS:
|
||||
- ReactCommon/turbomodule/core (= 0.73.7)
|
||||
- fmt (6.2.1)
|
||||
- glog (0.3.5)
|
||||
- GoogleSignIn (7.1.0):
|
||||
- AppAuth (< 2.0, >= 1.7.3)
|
||||
- GTMAppAuth (< 5.0, >= 4.1.1)
|
||||
- GTMSessionFetcher/Core (~> 3.3)
|
||||
- GTMAppAuth (4.1.1):
|
||||
- AppAuth/Core (~> 1.7)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- hermes-engine (0.73.7):
|
||||
- hermes-engine/Pre-built (= 0.73.7)
|
||||
- hermes-engine/Pre-built (0.73.7)
|
||||
@@ -1151,6 +1165,9 @@ PODS:
|
||||
- glog
|
||||
- RCT-Folly (= 2022.05.16.00)
|
||||
- React-Core
|
||||
- RNGoogleSignin (13.1.0):
|
||||
- GoogleSignIn (~> 7.1)
|
||||
- React-Core
|
||||
- RNLocalize (3.1.0):
|
||||
- React-Core
|
||||
- RNPermissions (4.1.5):
|
||||
@@ -1276,6 +1293,7 @@ DEPENDENCIES:
|
||||
- "RNFlashList (from `../node_modules/@shopify/flash-list`)"
|
||||
- RNFS (from `../node_modules/react-native-fs`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- "RNGoogleSignin (from `../node_modules/@react-native-google-signin/google-signin`)"
|
||||
- RNLocalize (from `../node_modules/react-native-localize`)
|
||||
- RNPermissions (from `../node_modules/react-native-permissions`)
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
@@ -1290,7 +1308,11 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- AppAuth
|
||||
- fmt
|
||||
- GoogleSignIn
|
||||
- GTMAppAuth
|
||||
- GTMSessionFetcher
|
||||
- libevent
|
||||
- MMKV
|
||||
- MMKVCore
|
||||
@@ -1461,6 +1483,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-fs"
|
||||
RNGestureHandler:
|
||||
:path: "../node_modules/react-native-gesture-handler"
|
||||
RNGoogleSignin:
|
||||
:path: "../node_modules/@react-native-google-signin/google-signin"
|
||||
RNLocalize:
|
||||
:path: "../node_modules/react-native-localize"
|
||||
RNPermissions:
|
||||
@@ -1485,6 +1509,7 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
|
||||
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
|
||||
BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70
|
||||
DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
|
||||
@@ -1493,6 +1518,9 @@ SPEC CHECKSUMS:
|
||||
FBReactNativeSpec: 40b791f4a1df779e7e4aa12c000319f4f216d40a
|
||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
|
||||
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
|
||||
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
hermes-engine: 39589e9c297d024e90fe68f6830ff86c4e01498a
|
||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||
MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801
|
||||
@@ -1571,6 +1599,7 @@ SPEC CHECKSUMS:
|
||||
RNFlashList: 818cb6cff1f47cabe1acd6298c98af1a39b9b18c
|
||||
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
|
||||
RNGestureHandler: e262eeb792addec0705a116456f210ee1be0dcd0
|
||||
RNGoogleSignin: ba93c1137f8d5cebdd39b04f493fd212ddf5ecd6
|
||||
RNLocalize: 080849cb8a824d9f759b8a5ae00c8321d46dbed0
|
||||
RNPermissions: f14c20f4eb7a20fff611ad9f467da7bb5872ac4f
|
||||
RNReanimated: b158619f02f1384a5be9e1203b58e0e4a80407d7
|
||||
@@ -1586,4 +1615,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: eff4b75123af5d6680139a78c055b44ad37c269b
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.14.3
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
<string>inaturalistmobile</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.796686868523-4m5ne09fa17p5me04s1qdlsnci9e8qeo</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>132</string>
|
||||
@@ -55,6 +63,8 @@
|
||||
<string>Add existing sounds to your observations.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>iNaturalist Next uses the camera to add photos to your observations of nature.</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© iNaturalist</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>We do not intentionally request this permission. If you are seeing this, please take a screenshot and email it to help@inaturalist.org</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
@@ -94,7 +104,5 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© iNaturalist</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -12,6 +12,7 @@ const config: Config = {
|
||||
preset: "react-native",
|
||||
setupFiles: [
|
||||
"./node_modules/react-native-gesture-handler/jestSetup.js",
|
||||
"./node_modules/@react-native-google-signin/google-signin/jest/build/jest/setup.js",
|
||||
"<rootDir>/tests/jest.setup.js"
|
||||
],
|
||||
globalSetup: "<rootDir>/tests/jest.globalSetup.js",
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"@react-native-community/hooks": "^3.0.0",
|
||||
"@react-native-community/netinfo": "^11.3.1",
|
||||
"@react-native-community/slider": "^4.5.0",
|
||||
"@react-native-google-signin/google-signin": "^13.1.0",
|
||||
"@react-native-picker/picker": "^2.7.2",
|
||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||
"@react-navigation/drawer": "^6.6.15",
|
||||
@@ -4893,6 +4894,25 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.0.tgz",
|
||||
"integrity": "sha512-pyUvNTvu5IfCI5abzqRfO/dd3A009RC66RXZE6t0gyOwI/j0QDlq9VZRv3rjkpuIvNTnsYj+m5BHlh0DkSYUyA=="
|
||||
},
|
||||
"node_modules/@react-native-google-signin/google-signin": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-13.1.0.tgz",
|
||||
"integrity": "sha512-C2/sqb0/s0c+Dwc/mykASZsRuHxGqn7SFrCxCY9D8p8IOQO05haInhCc7lzraJshRixGva5c/4usQZ71HMYSEQ==",
|
||||
"peerDependencies": {
|
||||
"expo": ">=50.0.0",
|
||||
"react": "*",
|
||||
"react-dom": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-picker/picker": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.7.2.tgz",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"@react-native-community/hooks": "^3.0.0",
|
||||
"@react-native-community/netinfo": "^11.3.1",
|
||||
"@react-native-community/slider": "^4.5.0",
|
||||
"@react-native-google-signin/google-signin": "^13.1.0",
|
||||
"@react-native-picker/picker": "^2.7.2",
|
||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||
"@react-navigation/drawer": "^6.6.15",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { appleAuth, AppleButton, AppleError } from "@invertase/react-native-apple-authentication";
|
||||
import { AppleButton } from "@invertase/react-native-apple-authentication";
|
||||
import { RouteProp, useNavigation, useRoute } from "@react-navigation/native";
|
||||
import classnames from "classnames";
|
||||
import {
|
||||
@@ -9,21 +9,16 @@ import { t } from "i18next";
|
||||
import { RealmContext } from "providers/contexts.ts";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
TextInput,
|
||||
TouchableWithoutFeedback
|
||||
} from "react-native";
|
||||
import Realm from "realm";
|
||||
import { log } from "sharedHelpers/logger";
|
||||
import useKeyboardInfo from "sharedHooks/useKeyboardInfo";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
import {
|
||||
authenticateUser,
|
||||
authenticateUserByAssertion
|
||||
} from "./AuthenticationService";
|
||||
import { authenticateUser } from "./AuthenticationService";
|
||||
import Error from "./Error";
|
||||
import { signInWithApple, signInWithGoogle } from "./loginFormHelpers";
|
||||
import LoginSignUpInputField from "./LoginSignUpInputField";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
@@ -42,85 +37,12 @@ type ParamList = {
|
||||
LoginFormParams: LoginFormParams
|
||||
}
|
||||
|
||||
interface AppleAuthError {
|
||||
code: AppleError;
|
||||
}
|
||||
|
||||
const APPLE_BUTTON_STYLE = {
|
||||
maxWidth: 500,
|
||||
height: 45, // You must specify a height
|
||||
marginTop: 10
|
||||
};
|
||||
|
||||
const logger = log.extend( "LoginForm" );
|
||||
|
||||
function showSignInWithAppleFailed() {
|
||||
Alert.alert(
|
||||
t( "Sign-in-with-Apple-Failed" ),
|
||||
t( "If-you-have-an-existing-account-try-sign-in-reset" )
|
||||
);
|
||||
}
|
||||
|
||||
async function signInWithApple( realm: Realm ) {
|
||||
// Request sign in w/ apple. This should pop up some system UI for signing
|
||||
// in
|
||||
let appleAuthRequestResponse;
|
||||
try {
|
||||
appleAuthRequestResponse = await appleAuth.performRequest( {
|
||||
requestedOperation: appleAuth.Operation.LOGIN,
|
||||
// Note: it appears putting FULL_NAME first is important, see issue #293
|
||||
requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL]
|
||||
} );
|
||||
} catch ( appleAuthRequestError ) {
|
||||
if ( ( appleAuthRequestError as AppleAuthError ).code === appleAuth.Error.CANCELED ) {
|
||||
// The user canceled sign in, no need to log
|
||||
return false;
|
||||
}
|
||||
logger.error( "Apple auth request failed", appleAuthRequestError );
|
||||
showSignInWithAppleFailed();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if auth was successful
|
||||
const credentialState = await appleAuth.getCredentialStateForUser(
|
||||
appleAuthRequestResponse.user
|
||||
);
|
||||
|
||||
// If it was, send the identity token to iNat for verification and iNat
|
||||
// auth
|
||||
if ( credentialState === appleAuth.State.AUTHORIZED ) {
|
||||
// Note that we're supporting an irregular assertion that is a JSON object
|
||||
// w/ the actual identityToken (which is itself a JSON Web Token), and
|
||||
// the user's name, which we only get from Apple the *first* time that
|
||||
// grant permission, so the server cannot access it when it verifies the
|
||||
// token
|
||||
const assertion = JSON.stringify( {
|
||||
id_token: appleAuthRequestResponse.identityToken,
|
||||
name: t( "apple-full-name", {
|
||||
namePrefix: appleAuthRequestResponse?.fullName?.namePrefix,
|
||||
givenName: appleAuthRequestResponse?.fullName?.givenName,
|
||||
middleName: appleAuthRequestResponse?.fullName?.middleName,
|
||||
nickname: appleAuthRequestResponse?.fullName?.nickname,
|
||||
familyName: appleAuthRequestResponse?.fullName?.familyName,
|
||||
nameSuffix: appleAuthRequestResponse?.fullName?.nameSuffix
|
||||
} )
|
||||
} );
|
||||
try {
|
||||
await authenticateUserByAssertion( "apple", assertion, realm );
|
||||
} catch ( authenticateUserByAssertionError ) {
|
||||
logger.error( "Assertion with Apple token failed", authenticateUserByAssertionError );
|
||||
showSignInWithAppleFailed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// We only get here if the user does not grant access... I think, so no need
|
||||
// to log an error
|
||||
logger.info( "Apple auth failed, credentialState: ", credentialState );
|
||||
showSignInWithAppleFailed();
|
||||
return false;
|
||||
}
|
||||
|
||||
const LoginForm = ( {
|
||||
hideFooter
|
||||
}: Props ) => {
|
||||
@@ -276,6 +198,12 @@ const LoginForm = ( {
|
||||
onPress={() => logIn( async ( ) => signInWithApple( realm ) )}
|
||||
/>
|
||||
) }
|
||||
<Button
|
||||
text={t( "Sign-in-with-Google" )}
|
||||
onPress={() => logIn( async ( ) => signInWithGoogle( realm ) )}
|
||||
disabled={loading}
|
||||
className="mt-3"
|
||||
/>
|
||||
{!hideFooter && (
|
||||
<Body1
|
||||
className={classnames(
|
||||
|
||||
158
src/components/LoginSignUp/loginFormHelpers.ts
Normal file
158
src/components/LoginSignUp/loginFormHelpers.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
// Helpers for LoginForm. Might be better in AuthenticationService, but
|
||||
// there's also some UI-related stuff in here, e.g. alerts
|
||||
import { appleAuth, AppleError } from "@invertase/react-native-apple-authentication";
|
||||
import {
|
||||
GoogleSignin,
|
||||
statusCodes as googleStatusCodes
|
||||
} from "@react-native-google-signin/google-signin";
|
||||
import { t } from "i18next";
|
||||
import { Alert } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import Realm from "realm";
|
||||
import { log } from "sharedHelpers/logger";
|
||||
|
||||
import {
|
||||
authenticateUserByAssertion
|
||||
} from "./AuthenticationService";
|
||||
|
||||
interface AppleAuthError {
|
||||
code: AppleError;
|
||||
}
|
||||
|
||||
const logger = log.extend( "loginFormHelpers" );
|
||||
|
||||
function showSignInWithAppleFailed() {
|
||||
Alert.alert(
|
||||
t( "Sign-in-with-Apple-Failed" ),
|
||||
t( "If-you-have-an-existing-account-try-sign-in-reset" )
|
||||
);
|
||||
}
|
||||
|
||||
async function signInWithApple( realm: Realm ) {
|
||||
// Request sign in w/ apple. This should pop up some system UI for signing
|
||||
// in
|
||||
let appleAuthRequestResponse;
|
||||
try {
|
||||
appleAuthRequestResponse = await appleAuth.performRequest( {
|
||||
requestedOperation: appleAuth.Operation.LOGIN,
|
||||
// Note: it appears putting FULL_NAME first is important, see issue #293
|
||||
requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL]
|
||||
} );
|
||||
} catch ( appleAuthRequestError ) {
|
||||
if ( ( appleAuthRequestError as AppleAuthError ).code === appleAuth.Error.CANCELED ) {
|
||||
// The user canceled sign in, no need to log
|
||||
return false;
|
||||
}
|
||||
logger.error( "Apple auth request failed", appleAuthRequestError );
|
||||
showSignInWithAppleFailed();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if auth was successful
|
||||
const credentialState = await appleAuth.getCredentialStateForUser(
|
||||
appleAuthRequestResponse.user
|
||||
);
|
||||
|
||||
// If it was, send the identity token to iNat for verification and iNat
|
||||
// auth
|
||||
if ( credentialState === appleAuth.State.AUTHORIZED ) {
|
||||
// Note that we're supporting an irregular assertion that is a JSON object
|
||||
// w/ the actual identityToken (which is itself a JSON Web Token), and
|
||||
// the user's name, which we only get from Apple the *first* time that
|
||||
// grant permission, so the server cannot access it when it verifies the
|
||||
// token
|
||||
const assertion = JSON.stringify( {
|
||||
id_token: appleAuthRequestResponse.identityToken,
|
||||
name: t( "apple-full-name", {
|
||||
namePrefix: appleAuthRequestResponse?.fullName?.namePrefix,
|
||||
givenName: appleAuthRequestResponse?.fullName?.givenName,
|
||||
middleName: appleAuthRequestResponse?.fullName?.middleName,
|
||||
nickname: appleAuthRequestResponse?.fullName?.nickname,
|
||||
familyName: appleAuthRequestResponse?.fullName?.familyName,
|
||||
nameSuffix: appleAuthRequestResponse?.fullName?.nameSuffix
|
||||
} )
|
||||
} );
|
||||
try {
|
||||
await authenticateUserByAssertion( "apple", assertion, realm );
|
||||
} catch ( authenticateUserByAssertionError ) {
|
||||
logger.error( "Assertion with Apple token failed", authenticateUserByAssertionError );
|
||||
showSignInWithAppleFailed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// We only get here if the user does not grant access... I think, so no need
|
||||
// to log an error
|
||||
logger.info( "Apple auth failed, credentialState: ", credentialState );
|
||||
showSignInWithAppleFailed();
|
||||
return false;
|
||||
}
|
||||
|
||||
GoogleSignin.configure( {
|
||||
iosClientId: Config.GOOGLE_IOS_CLIENT_ID,
|
||||
webClientId: Config.GOOGLE_WEB_CLIENT_ID,
|
||||
scopes: [
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile"
|
||||
]
|
||||
} );
|
||||
|
||||
async function confirmGooglePlayServices() {
|
||||
try {
|
||||
await GoogleSignin.hasPlayServices( );
|
||||
return true;
|
||||
} catch ( hasPlayServicesError ) {
|
||||
if (
|
||||
( hasPlayServicesError as { code: string } ).code
|
||||
=== googleStatusCodes.PLAY_SERVICES_NOT_AVAILABLE
|
||||
) {
|
||||
Alert.alert(
|
||||
t( "Google-Play-Services-Not-Installed" ),
|
||||
t( "You-must-install-Google-Play-Services-to-sign-in-with-Google" )
|
||||
);
|
||||
return false;
|
||||
}
|
||||
throw hasPlayServicesError;
|
||||
}
|
||||
}
|
||||
|
||||
function showSignInWithGoogleFailed() {
|
||||
Alert.alert(
|
||||
t( "Sign-in-with-Google-Failed" ),
|
||||
t( "If-you-have-an-existing-account-try-sign-in-reset" )
|
||||
);
|
||||
}
|
||||
|
||||
async function signInWithGoogle( realm: Realm ) {
|
||||
const hasPlayServices = await confirmGooglePlayServices( );
|
||||
if ( !hasPlayServices ) return false;
|
||||
let signInResp;
|
||||
try {
|
||||
signInResp = await GoogleSignin.signIn();
|
||||
} catch ( signInError ) {
|
||||
logger.error( "Failed to sign in with Google", signInError );
|
||||
return false;
|
||||
}
|
||||
if ( signInResp.type === "cancelled" ) return false;
|
||||
let tokens;
|
||||
try {
|
||||
tokens = await GoogleSignin.getTokens();
|
||||
} catch ( getTokensError ) {
|
||||
logger.error( "Failed to get tokens from Google", getTokensError );
|
||||
return false;
|
||||
}
|
||||
if ( !tokens?.accessToken ) return false;
|
||||
try {
|
||||
await authenticateUserByAssertion( "google", tokens.accessToken, realm );
|
||||
} catch ( authenticateUserByAssertionError ) {
|
||||
logger.error( "Assertion with Google token failed", authenticateUserByAssertionError );
|
||||
showSignInWithGoogleFailed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export {
|
||||
signInWithApple,
|
||||
signInWithGoogle
|
||||
};
|
||||
@@ -22,7 +22,7 @@ interface ButtonProps {
|
||||
testID?: string;
|
||||
text: string;
|
||||
dropdown?: boolean;
|
||||
maxFontSizeMultiplier: number
|
||||
maxFontSizeMultiplier?: number
|
||||
}
|
||||
|
||||
const setStyles = ( {
|
||||
|
||||
@@ -513,6 +513,7 @@ Get-more-accurate-suggestions-create-useful-data = Get more accurate suggestions
|
||||
Get-your-identification-verified-by-real-people = Get your identification verified by real people in the iNaturalist community
|
||||
# Label for button that returns to the previous screen
|
||||
Go-back = Go back
|
||||
Google-Play-Services-Not-Installed = Google Play Services Not Installed
|
||||
# Text for a button that asks the user to grant permission
|
||||
GRANT-PERMISSION = GRANT PERMISSION
|
||||
# Title of a screen asking for permission
|
||||
@@ -1110,6 +1111,9 @@ Shows-iNaturalist-bird-logo = Shows iNaturalist bird logo.
|
||||
Shows-observation-creation-options = Shows observation creation options
|
||||
# Title of an error alert when Sign in with Apple fails
|
||||
Sign-in-with-Apple-Failed = Sign in with Apple Failed
|
||||
# Label for button that allows user to sign in with their Google account
|
||||
Sign-in-with-Google = Sign in with Google
|
||||
Sign-in-with-Google-Failed = Sign in with Google Failed
|
||||
Some-data-privacy-laws = Some data privacy laws, like the European Union's General Data Protection Regulation (GDPR), require explicit consent to transfer personal information from their jurisdictions to other jurisdictions where the legal protection of this information is not considered adequate. As of 2020, the European Union no longer considers the United States to be a jurisdiction that provides adequate legal protection of personal information, specifically because of the possibility of the US government surveilling data entering the US. It is possible other jurisdictions may have the same opinion.
|
||||
# Generic error message
|
||||
Something-went-wrong = Something went wrong.
|
||||
@@ -1454,6 +1458,7 @@ You-changed-filters-will-be-discarded = You changed filters, but they were not a
|
||||
You-have-opted-out-of-the-Community-Taxon = You have opted out of the Community Taxon
|
||||
You-havent-joined-any-projects-yet = You haven’t joined any projects yet!
|
||||
You-must-be-logged-in-to-view-messages = You must be logged in to view messages
|
||||
You-must-install-Google-Play-Services-to-sign-in-with-Google = You must install Google Play Services to sign in with Google.
|
||||
# Error message when you try to do something that requires an Internet
|
||||
# connection but such a connection is, tragically, missing
|
||||
You-need-an-Internet-connection-to-do-that = You need an Internet connection to do that.
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"Get-more-accurate-suggestions-create-useful-data": "Get more accurate suggestions & create useful data for science using your location",
|
||||
"Get-your-identification-verified-by-real-people": "Get your identification verified by real people in the iNaturalist community",
|
||||
"Go-back": "Go back",
|
||||
"Google-Play-Services-Not-Installed": "Google Play Services Not Installed",
|
||||
"GRANT-PERMISSION": "GRANT PERMISSION",
|
||||
"Grant-Permission-title": "Grant Permission",
|
||||
"Grid-layout": "Grid layout",
|
||||
@@ -714,6 +715,8 @@
|
||||
"Shows-iNaturalist-bird-logo": "Shows iNaturalist bird logo.",
|
||||
"Shows-observation-creation-options": "Shows observation creation options",
|
||||
"Sign-in-with-Apple-Failed": "Sign in with Apple Failed",
|
||||
"Sign-in-with-Google": "Sign in with Google",
|
||||
"Sign-in-with-Google-Failed": "Sign in with Google Failed",
|
||||
"Some-data-privacy-laws": "Some data privacy laws, like the European Union's General Data Protection Regulation (GDPR), require explicit consent to transfer personal information from their jurisdictions to other jurisdictions where the legal protection of this information is not considered adequate. As of 2020, the European Union no longer considers the United States to be a jurisdiction that provides adequate legal protection of personal information, specifically because of the possibility of the US government surveilling data entering the US. It is possible other jurisdictions may have the same opinion.",
|
||||
"Something-went-wrong": "Something went wrong.",
|
||||
"Sorry-this-observation-was-deleted": "Sorry, this observation was deleted",
|
||||
@@ -895,6 +898,7 @@
|
||||
"You-have-opted-out-of-the-Community-Taxon": "You have opted out of the Community Taxon",
|
||||
"You-havent-joined-any-projects-yet": "You haven’t joined any projects yet!",
|
||||
"You-must-be-logged-in-to-view-messages": "You must be logged in to view messages",
|
||||
"You-must-install-Google-Play-Services-to-sign-in-with-Google": "You must install Google Play Services to sign in with Google.",
|
||||
"You-need-an-Internet-connection-to-do-that": "You need an Internet connection to do that.",
|
||||
"You-need-log-in-to-do-that": "You need to log in to do that.",
|
||||
"You-will-see-notifications": "You’ll see notifications here once you log in & upload observations.",
|
||||
|
||||
@@ -513,6 +513,7 @@ Get-more-accurate-suggestions-create-useful-data = Get more accurate suggestions
|
||||
Get-your-identification-verified-by-real-people = Get your identification verified by real people in the iNaturalist community
|
||||
# Label for button that returns to the previous screen
|
||||
Go-back = Go back
|
||||
Google-Play-Services-Not-Installed = Google Play Services Not Installed
|
||||
# Text for a button that asks the user to grant permission
|
||||
GRANT-PERMISSION = GRANT PERMISSION
|
||||
# Title of a screen asking for permission
|
||||
@@ -1110,6 +1111,9 @@ Shows-iNaturalist-bird-logo = Shows iNaturalist bird logo.
|
||||
Shows-observation-creation-options = Shows observation creation options
|
||||
# Title of an error alert when Sign in with Apple fails
|
||||
Sign-in-with-Apple-Failed = Sign in with Apple Failed
|
||||
# Label for button that allows user to sign in with their Google account
|
||||
Sign-in-with-Google = Sign in with Google
|
||||
Sign-in-with-Google-Failed = Sign in with Google Failed
|
||||
Some-data-privacy-laws = Some data privacy laws, like the European Union's General Data Protection Regulation (GDPR), require explicit consent to transfer personal information from their jurisdictions to other jurisdictions where the legal protection of this information is not considered adequate. As of 2020, the European Union no longer considers the United States to be a jurisdiction that provides adequate legal protection of personal information, specifically because of the possibility of the US government surveilling data entering the US. It is possible other jurisdictions may have the same opinion.
|
||||
# Generic error message
|
||||
Something-went-wrong = Something went wrong.
|
||||
@@ -1454,6 +1458,7 @@ You-changed-filters-will-be-discarded = You changed filters, but they were not a
|
||||
You-have-opted-out-of-the-Community-Taxon = You have opted out of the Community Taxon
|
||||
You-havent-joined-any-projects-yet = You haven’t joined any projects yet!
|
||||
You-must-be-logged-in-to-view-messages = You must be logged in to view messages
|
||||
You-must-install-Google-Play-Services-to-sign-in-with-Google = You must install Google Play Services to sign in with Google.
|
||||
# Error message when you try to do something that requires an Internet
|
||||
# connection but such a connection is, tragically, missing
|
||||
You-need-an-Internet-connection-to-do-that = You need an Internet connection to do that.
|
||||
|
||||
Reference in New Issue
Block a user