Skip to content

Commit

Permalink
feat: refactored js apis (#1326)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: function signatures have changed
  • Loading branch information
vonovak committed Sep 1, 2024
1 parent 708eda1 commit 04c3c64
Show file tree
Hide file tree
Showing 14 changed files with 372 additions and 118 deletions.
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 @@ export class GoogleSigninSampleApp extends Component<{}, State> {

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 @@ -156,25 +156,28 @@ export class GoogleSigninSampleApp extends Component<{}, State> {
<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 Down Expand Up @@ -210,6 +213,18 @@ export class GoogleSigninSampleApp extends Component<{}, State> {
});
}
};

_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

0 comments on commit 04c3c64

Please sign in to comment.