Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new js api #1326

Merged
merged 5 commits into from
Sep 1, 2024
Merged
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
20 changes: 10 additions & 10 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@ People discussing in this GitHub organization are expected to interact in ways t
Examples of behavior that contributes to a positive environment for our
community include:

* Giving and accepting constructive feedback
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Accepting responsibility and apologizing to those affected by our mistakes,
- Giving and accepting constructive feedback
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience

Examples of unacceptable behavior include:

* Trolling, insulting or derogatory comments, and personal or political attacks
* Making unfounded statements that put people or projects in bad light
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Making unfounded statements that put people or projects in bad light
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* The use of sexualized language or imagery, and sexual attention or
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting

## Enforcement
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,7 @@ PODS:
- React-Core
- React-jsi
- ReactTestApp-Resources (1.0.0-dev)
- RNGoogleSignin (11.0.1):
- RNGoogleSignin (12.2.1):
- GoogleSignIn (~> 7.1)
- React-Core
- SocketRocket (0.7.0)
Expand Down Expand Up @@ -1460,7 +1460,7 @@ SPEC CHECKSUMS:
ReactNativeHost: a365307db1ece0c4825b9d0f8b35de1bb2a61b0a
ReactTestApp-DevSupport: f845db38b4b4ce8d341f8acdba934ee85ed3d7b2
ReactTestApp-Resources: 3171451c647ad9dbb037146693ea8046a58cb638
RNGoogleSignin: b78e49de632b5982a4c7d42d2e6b59cc4b8493c0
RNGoogleSignin: 08dc4ba7eac2096c7fecfe109f0950fa4683c803
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: b9a182ab00cf25926e7f79657d08c5d23c2d03b0

Expand Down
47 changes: 31 additions & 16 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,17 @@

async _getCurrentUser() {
try {
const userInfo = await GoogleSignin.signInSilently();
this.setState({ userInfo, error: undefined });
} catch (error) {
const typedError = error as NativeModuleError;
if (typedError.code === statusCodes.SIGN_IN_REQUIRED) {
const { type, data } = await GoogleSignin.signInSilently();
if (type === 'success') {
this.setState({ userInfo: data, error: undefined });
} else if (type === 'noSavedCredentialFound') {
this.setState({
error: new Error('User not signed it yet, please sign in :)'),
error: new Error('User not signed in yet, please sign in :)'),
});
} else {
this.setState({ error: typedError });
}
} catch (error) {
const typedError = error as NativeModuleError;
this.setState({ error: typedError });
}
}

Expand Down Expand Up @@ -140,7 +140,7 @@
return (
<View style={styles.container}>
<Text style={styles.welcomeText}>Welcome, {userInfo.user.name}</Text>
<Text selectable style={{ color: 'black' }}>

Check warning on line 143 in example/src/App.tsx

View workflow job for this annotation

GitHub Actions / build

Inline style: { color: 'black' }
Your user info:{' '}
{prettyJson({
...userInfo,
Expand All @@ -156,25 +156,28 @@
<TokenClearingView />

<Button onPress={this._signOut} title="Log out" />
<Button onPress={this._revokeAccess} title="Revoke access" />
</View>
);
}

_signIn = async () => {
try {
await GoogleSignin.hasPlayServices();
const userInfo = await GoogleSignin.signIn();
this.setState({ userInfo, error: undefined });
const { type, data } = await GoogleSignin.signIn();
if (type === 'success') {
console.log({ data });
this.setState({ userInfo: data, error: undefined });
} else {
// sign in was cancelled by user
setTimeout(() => {
Alert.alert('cancelled');
}, 500);
}
} catch (error) {
if (isErrorWithCode(error)) {
console.log('error', error.message);
switch (error.code) {
case statusCodes.SIGN_IN_CANCELLED:
// sign in was cancelled by user
setTimeout(() => {
Alert.alert('cancelled');
}, 500);
break;
case statusCodes.IN_PROGRESS:
// operation (eg. sign in) already in progress
Alert.alert(
Expand All @@ -193,7 +196,7 @@
error,
});
} else {
alert(`an error that's not related to google sign in occurred`);

Check warning on line 199 in example/src/App.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected alert
}
}
};
Expand All @@ -210,6 +213,18 @@
});
}
};

_revokeAccess = async () => {
try {
await GoogleSignin.revokeAccess();

this.setState({ userInfo: undefined, error: undefined });
} catch (error) {
this.setState({
error: error as NativeModuleError,
});
}
};
}

const styles = StyleSheet.create({
Expand Down
28 changes: 22 additions & 6 deletions index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,31 @@ type StatusCodes = $ReadOnly<{

declare export var statusCodes: StatusCodes;

// the functions are not static in fact, but the module exports a
// singleton instance of the class; not the class itself
// using static keyword works well for this case
export type SignInSuccessResponse = {|
type: 'success';
data: User;
|};
export type CancelledResponse = {|
type: 'cancelled';
data: null;
|};
export type NoSavedCredentialFound = {|
type: 'noSavedCredentialFound';
data: null;
|};
export type SignInResponse = SignInSuccessResponse | CancelledResponse;
export type SignInSilentlyResponse =
| SignInSuccessResponse
| NoSavedCredentialFound;

// the functions are not static class functions in fact
// but using static keyword works well for this case
declare export class GoogleSignin {
static hasPlayServices: (params?: HasPlayServicesParams) => Promise<boolean>;
static configure: (params?: ConfigureParams) => void;
static signInSilently: () => Promise<User>;
static signIn: (params?: SignInParams) => Promise<User>;
static addScopes: (params: AddScopesParams) => Promise<User | null>;
static signInSilently: () => Promise<SignInSilentlyResponse>;
static signIn: (params?: SignInParams) => Promise<SignInResponse>;
static addScopes: (params: AddScopesParams) => Promise<SignInResponse | null>;
static signOut: () => Promise<null>;
static revokeAccess: () => Promise<null>;
static hasPreviousSignIn: () => boolean;
Expand Down
121 changes: 62 additions & 59 deletions jest/setup.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React from 'react';
import { Pressable, Text } from 'react-native';
import type { Spec as GoogleSignInSpec } from '../src/spec/NativeGoogleSignin';

import type {
GoogleSigninButton,
GoogleSigninButtonProps,
AddScopesParams,
GetTokensResponse,
SignInResponse,
User,
GoogleSignin,
} from '../src';
import type { statusCodes } from '../src';
import { isErrorWithCode } from '../src/types';

export const mockUserInfo: User = {
export const mockUserInfo = Object.freeze({
idToken: 'mockIdToken',
serverAuthCode: 'mockServerAuthCode',
scopes: [],
Expand All @@ -21,57 +19,62 @@ export const mockUserInfo: User = {
photo: null,
name: 'mockFullName',
},
};

const MockGoogleSigninButton = (props: GoogleSigninButtonProps) => {
return (
<Pressable {...props}>
<Text>Mock Sign in with Google</Text>
</Pressable>
);
};
MockGoogleSigninButton.Size = { Standard: 0, Wide: 1, Icon: 2 };
MockGoogleSigninButton.Color = { Dark: 'dark', Light: 'light' } as const;

const MockGoogleSigninButtonTyped: typeof GoogleSigninButton =
MockGoogleSigninButton;

const mockStatusCodesRaw: typeof statusCodes = {
SIGN_IN_CANCELLED: 'mock_SIGN_IN_CANCELLED',
IN_PROGRESS: 'mock_IN_PROGRESS',
PLAY_SERVICES_NOT_AVAILABLE: 'mock_PLAY_SERVICES_NOT_AVAILABLE',
SIGN_IN_REQUIRED: 'mock_SIGN_IN_REQUIRED',
};
}) satisfies User;

const mockStatusCodes = Object.freeze(mockStatusCodesRaw);

const mockGoogleSignin: typeof GoogleSignin = {
configure: jest.fn(),
hasPlayServices: jest.fn().mockResolvedValue(true),
getTokens: jest.fn().mockResolvedValue({
accessToken: 'mockAccessToken',
idToken: 'mockIdToken',
}),
signIn: jest.fn().mockResolvedValue(mockUserInfo),
signInSilently: jest.fn().mockResolvedValue(mockUserInfo),
revokeAccess: jest.fn().mockResolvedValue(null),
signOut: jest.fn().mockResolvedValue(null),
hasPreviousSignIn: jest.fn().mockReturnValue(true),
addScopes: jest.fn().mockResolvedValue(mockUserInfo),
getCurrentUser: jest.fn().mockReturnValue(mockUserInfo),
clearCachedAccessToken: jest.fn().mockResolvedValue(null),
};

type ExportedModuleType = typeof import('../src/index');

// TODO @vonovak mock closer to native level?
const mockModule: ExportedModuleType = Object.freeze({
statusCodes: mockStatusCodes,
GoogleSignin: mockGoogleSignin,
GoogleSigninButton: MockGoogleSigninButtonTyped,
isErrorWithCode,
});
export const mockGoogleSignInResponse: SignInResponse = Object.freeze({
type: 'success',
data: mockUserInfo,
} satisfies SignInResponse);

jest.mock('@react-native-google-signin/google-signin', () => {
return mockModule;
// mock very close to native module to be able to test JS logic too
jest.mock('../src/spec/NativeGoogleSignin', () => {
const mockNativeModule: GoogleSignInSpec = Object.freeze({
configure: jest.fn(),
playServicesAvailable: jest.fn().mockReturnValue(true),
getTokens: jest
.fn<Promise<GetTokensResponse>, Object[]>()
.mockResolvedValue({
accessToken: 'mockAccessToken',
idToken: 'mockIdToken',
}),
signIn: jest.fn<Promise<User>, Object[]>().mockResolvedValue(mockUserInfo),
signInSilently: jest
.fn<Promise<User>, Object[]>()
.mockResolvedValue(mockUserInfo),
revokeAccess: jest.fn().mockResolvedValue(null),
signOut: jest.fn().mockResolvedValue(null),
// enableAppCheck: jest.fn().mockResolvedValue(null),
hasPreviousSignIn: jest.fn().mockReturnValue(true),
addScopes: jest
.fn<Promise<User | null>, AddScopesParams[]>()
.mockImplementation(({ scopes }) => {
const userWithScopes: User = {
...mockUserInfo,
scopes,
};
return Promise.resolve(userWithScopes);
}),
getCurrentUser: jest
.fn<User | null, void[]>()
.mockReturnValue(mockUserInfo),
clearCachedAccessToken: jest.fn().mockResolvedValue(null),
getConstants: jest
.fn<ReturnType<GoogleSignInSpec['getConstants']>, void[]>()
.mockReturnValue({
SIGN_IN_CANCELLED: 'mock_SIGN_IN_CANCELLED',
IN_PROGRESS: 'mock_IN_PROGRESS',
PLAY_SERVICES_NOT_AVAILABLE: 'mock_PLAY_SERVICES_NOT_AVAILABLE',
SIGN_IN_REQUIRED: 'mock_SIGN_IN_REQUIRED',
SCOPES_ALREADY_GRANTED: 'mock_SCOPES_ALREADY_GRANTED',
NO_SAVED_CREDENTIAL_FOUND: 'mock_NO_SAVED_CREDENTIAL_FOUND',
BUTTON_SIZE_ICON: 2,
BUTTON_SIZE_WIDE: 1,
BUTTON_SIZE_STANDARD: 0,
// one-tap specific constants
ONE_TAP_START_FAILED: 'mock_ONE_TAP_START_FAILED',
}),
});
return {
NativeModule: mockNativeModule,
};
});
9 changes: 7 additions & 2 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
GoogleSigninButton,
isErrorWithCode,
} from '@react-native-google-signin/google-signin';
import { mockUserInfo } from '../../jest/setup';
import { mockGoogleSignInResponse, mockUserInfo } from '../../jest/setup';

describe('GoogleSignin', () => {
describe('sanity checks for exported mocks', () => {
Expand All @@ -20,7 +20,12 @@ describe('GoogleSignin', () => {

it('original sign in', async () => {
expect(GoogleSignin.hasPreviousSignIn()).toBe(true);
expect(await GoogleSignin.signIn()).toStrictEqual(mockUserInfo);
expect(await GoogleSignin.signIn()).toStrictEqual(
mockGoogleSignInResponse,
);
expect(await GoogleSignin.signInSilently()).toStrictEqual(
mockGoogleSignInResponse,
);
expect(GoogleSignin.getCurrentUser()).toStrictEqual(mockUserInfo);
expect(await GoogleSignin.signOut()).toBeNull();
expect(GoogleSigninButton).toBeInstanceOf(Function);
Expand Down
9 changes: 9 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const cancelledResult = Object.freeze({
type: 'cancelled',
data: null,
});

export const noSavedCredentialFoundResult = Object.freeze({
type: 'noSavedCredentialFound',
data: null,
});
5 changes: 5 additions & 0 deletions src/errors/errorCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ const {
IN_PROGRESS,
PLAY_SERVICES_NOT_AVAILABLE,
SIGN_IN_REQUIRED,
SCOPES_ALREADY_GRANTED,
} = NativeModule.getConstants();

export const SIGN_IN_REQUIRED_CODE = SIGN_IN_REQUIRED;
export const SIGN_IN_CANCELLED_CODE = SIGN_IN_CANCELLED;
export const ios_only_SCOPES_ALREADY_GRANTED = SCOPES_ALREADY_GRANTED;

/**
* @group Constants
* */
Expand Down
Loading
Loading