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

Create Auth API #4

Merged
merged 28 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b816969
Create Auth API
orzechdev May 29, 2020
794ea8e
Update graphql types
orzechdev May 29, 2020
3b0d38e
Fix graphql types
orzechdev Jun 1, 2020
9a2d385
Await password credential api
orzechdev Jun 1, 2020
ef3ab97
Handle no credential api exception
orzechdev Jun 1, 2020
008a8d9
Initialize apollo client in saleor provider
orzechdev Jun 3, 2020
c526aa3
Add apollo provider in saleor provider
orzechdev Jun 3, 2020
b992932
Update readme
orzechdev Jun 3, 2020
bc1e11f
Update readme
orzechdev Jun 4, 2020
991f06f
update readme
orzechdev Jun 5, 2020
f0fc968
Merge branch 'master' into refactor/user-auth
orzechdev Jul 6, 2020
4caaedd
Update credential management api types
orzechdev Jul 6, 2020
c54a8f8
Update readme
orzechdev Jul 6, 2020
206ed13
Update SaleorProvider
orzechdev Jul 6, 2020
d8b4ca6
Change CustomConfig to ConfigInput
orzechdev Jul 6, 2020
1a14f8a
Implement custom apollo configuration for saleor provider
orzechdev Jul 6, 2020
cab8463
Refactor Apollo client initialization
orzechdev Jul 7, 2020
d7c71a4
Update tests
orzechdev Jul 7, 2020
450de9b
Fix LocalStorageHandler reference error
orzechdev Jul 7, 2020
f9b6858
Merge branch 'master' into refactor/user-auth
orzechdev Jul 7, 2020
38550ed
Fix imports
orzechdev Jul 7, 2020
b062967
Update sdk config docs strings
orzechdev Jul 9, 2020
195698a
No cache sign in user
orzechdev Jul 9, 2020
7c9e331
Remove user details from tokenAuth and verifyToken mutations
orzechdev Jul 9, 2020
8d4241e
Load checkout conditionally on sign in
orzechdev Jul 9, 2020
c972e78
Remove FunctionErrorAuthTypes
orzechdev Jul 9, 2020
d75a6cd
Update useHook
orzechdev Jul 9, 2020
8698c0a
Update readme
orzechdev Jul 10, 2020
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
102 changes: 40 additions & 62 deletions src/api/APIProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import {
} from "apollo-client";
import { GraphQLError } from "graphql";

import { fireSignOut, getAuthToken, setAuthToken } from "../auth";
import { getAuthToken, setAuthToken } from "../auth";
import { MUTATIONS } from "../mutations";
import { TokenAuth } from "../mutations/gqlTypes/TokenAuth";
import { QUERIES } from "../queries";
import { UserDetails } from "../queries/gqlTypes/UserDetails";
import { RequireAtLeastOne } from "../tsHelpers";
import {
InferOptions,
Expand All @@ -25,97 +24,80 @@ import {
isDataEmpty,
mergeEdges,
} from "../utils";
import { BROWSER_NO_CREDENTIAL_API_MESSAGE } from "./Auth";

export class APIProxy {
getAttributes = this.watchQuery(QUERIES.Attributes, data => data.attributes);
getAttributes = this.watchQuery(
QUERIES.Attributes,
(data) => data.attributes
);

getProductDetails = this.watchQuery(
QUERIES.ProductDetails,
data => data.product
(data) => data.product
);

getProductList = this.watchQuery(QUERIES.ProductList, data => data.products);
getProductList = this.watchQuery(
QUERIES.ProductList,
(data) => data.products
);

getCategoryDetails = this.watchQuery(
QUERIES.CategoryDetails,
data => data.category
(data) => data.category
);

getOrdersByUser = this.watchQuery(QUERIES.OrdersByUser, data =>
getOrdersByUser = this.watchQuery(QUERIES.OrdersByUser, (data) =>
data.me ? data.me.orders : null
);

getOrderDetails = this.watchQuery(
QUERIES.OrderDetails,
data => data.orderByToken
(data) => data.orderByToken
);

getVariantsProducts = this.watchQuery(
QUERIES.VariantsProducts,
data => data.productVariants
(data) => data.productVariants
);

getShopDetails = this.watchQuery(QUERIES.GetShopDetails, data => data);
getShopDetails = this.watchQuery(QUERIES.GetShopDetails, (data) => data);

setUserDefaultAddress = this.fireQuery(
MUTATIONS.AddressTypeUpdate,
data => data!.accountSetDefaultAddress
(data) => data!.accountSetDefaultAddress
);

setDeleteUserAddress = this.fireQuery(
MUTATIONS.DeleteUserAddress,
data => data!.accountAddressDelete
(data) => data!.accountAddressDelete
);

setCreateUserAddress = this.fireQuery(
MUTATIONS.CreateUserAddress,
data => data!.accountAddressCreate
(data) => data!.accountAddressCreate
);

setUpdateuserAddress = this.fireQuery(
MUTATIONS.UpdateUserAddress,
data => data!.accountAddressUpdate
(data) => data!.accountAddressUpdate
);

setAccountUpdate = this.fireQuery(
MUTATIONS.AccountUpdate,
data => data!.accountUpdate
(data) => data!.accountUpdate
);

setPasswordChange = this.fireQuery(MUTATIONS.PasswordChange, data => data);
setPasswordChange = this.fireQuery(MUTATIONS.PasswordChange, (data) => data);

setPassword = this.fireQuery(MUTATIONS.SetPassword, data => data);
setPassword = this.fireQuery(MUTATIONS.SetPassword, (data) => data);

client: ApolloClient<any>;

constructor(client: ApolloClient<any>) {
this.client = client;
}

getUserDetails = (
variables: InferOptions<QUERIES["UserDetails"]>["variables"],
options: Omit<InferOptions<QUERIES["UserDetails"]>, "variables"> & {
onUpdate: (data: UserDetails["me"] | null) => void;
}
) => {
if (this.isLoggedIn()) {
return this.watchQuery(QUERIES.UserDetails, data => data.me)(
variables,
options
);
}
if (options.onUpdate) {
options.onUpdate(null);
}
return {
refetch: () =>
new Promise<{ data: UserDetails["me"] }>((resolve, _reject) => {
resolve({ data: null });
}),
unsubscribe: () => undefined,
};
};

signIn = (
variables: InferOptions<MUTATIONS["TokenAuth"]>["variables"],
options?: Omit<InferOptions<MUTATIONS["TokenAuth"]>, "variables">
Expand All @@ -126,7 +108,7 @@ export class APIProxy {

const data = await this.fireQuery(
MUTATIONS.TokenAuth,
data => data!.tokenCreate
(data) => data!.tokenCreate
)(variables, {
...options,
update: (proxy, data) => {
Expand All @@ -138,12 +120,19 @@ export class APIProxy {
if (!handledData.errors && handledData.data) {
setAuthToken(handledData.data.token);
if (window.PasswordCredential && variables) {
navigator.credentials.store(
new window.PasswordCredential({
id: variables.email,
password: variables.password,
})
);
navigator.credentials
.store(
new window.PasswordCredential({
id: variables.email,
password: variables.password,
})
)
.catch((credentialsError) =>
console.warn(
BROWSER_NO_CREDENTIAL_API_MESSAGE,
credentialsError
)
);
}
}
if (options && options.update) {
Expand All @@ -158,17 +147,6 @@ export class APIProxy {
}
});

signOut = () =>
new Promise(async (resolve, reject) => {
try {
fireSignOut(this.client);

resolve();
} catch (e) {
reject(e);
}
});

attachAuthListener = (callback: (authenticated: boolean) => void) => {
const eventHandler = () => {
callback(this.isLoggedIn());
Expand Down Expand Up @@ -226,7 +204,7 @@ export class APIProxy {
}

const subscription = observable.subscribe(
result => {
(result) => {
const { data, errors: apolloErrors } = result;
const errorHandledData = handleDataErrors(
mapFn,
Expand All @@ -246,7 +224,7 @@ export class APIProxy {
}
}
},
error => {
(error) => {
if (onError) {
onError(error);
}
Expand Down Expand Up @@ -281,7 +259,7 @@ export class APIProxy {
);

// use new result for metadata and mutate existing data
Object.keys(prevResultRef).forEach(key => {
Object.keys(prevResultRef).forEach((key) => {
prevResultRef[key] = newResultRef[key];
});
prevResultRef.edges = mergedEdges;
Expand Down
148 changes: 148 additions & 0 deletions src/api/Auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { User } from "../../fragments/gqlTypes/User";
import { ErrorListener } from "../../helpers";
import { JobsManager } from "../../jobs";
import { SaleorState, SaleorStateLoaded } from "../../state";
import { StateItems } from "../../state/types";

import { PromiseRunResponse } from "../types";
import { DataErrorAuthTypes, FunctionErrorAuthTypes } from "./types";

export const BROWSER_NO_CREDENTIAL_API_MESSAGE =
"Saleor SDK is unable to use browser Credential Management API.";

export class AuthAPI extends ErrorListener {
/**
* Indicates if data is initialized, initially retrieved from cache or initially fetched.
*/
loaded: boolean;
/**
* User object with currently signed in user data.
*/
user?: User | null;
/**
* Indicates if user is signed in.
*/
authenticated?: boolean;
/**
* Token used for user authentication.
*/
token?: string;

private saleorState: SaleorState;
private jobsManager: JobsManager;

constructor(saleorState: SaleorState, jobsManager: JobsManager) {
super();
this.saleorState = saleorState;
this.jobsManager = jobsManager;

this.loaded = false;

this.saleorState.subscribeToChange(StateItems.USER, (user: User | null) => {
this.user = user;
if (this.loaded) {
this.authenticated = !!this.user;
}
});
this.saleorState.subscribeToChange(StateItems.SIGN_IN_TOKEN, (token) => {
this.token = token;
});
this.saleorState.subscribeToChange(
StateItems.LOADED,
(loaded: SaleorStateLoaded) => {
this.loaded = loaded.user && loaded.signInToken;
if (this.loaded) {
this.authenticated = !!this.user;
}
}
);

if (!this.saleorState.signInToken && window.PasswordCredential) {
this.autoSignIn();
}
}

/**
* Tries to authenticate user with given email and password.
* @param email Email used for authentication.
* @param password Password used for authentication.
* @param autoSignIn Indicates if SDK should try to sign in user with given credentials in future without explicitly calling this method. True by default.
*/
signIn = async (
email: string,
password: string,
autoSignIn: boolean = true
): PromiseRunResponse<DataErrorAuthTypes, FunctionErrorAuthTypes> => {
const { data, dataError } = await this.jobsManager.run("auth", "signIn", {
email,
password,
});

try {
if (autoSignIn && !dataError?.error && window.PasswordCredential) {
await navigator.credentials.store(
new window.PasswordCredential({
id: email,
password,
})
);
}
} catch (credentialsError) {
console.warn(BROWSER_NO_CREDENTIAL_API_MESSAGE, credentialsError);
}

await this.jobsManager.run("checkout", "provideCheckout", {
isUserSignedIn: !!data?.user,
});

return {
data,
dataError,
pending: false,
};
};

/**
* Sign out user by clearing cache, local storage and authentication token.
*/
signOut = async (): PromiseRunResponse<
DataErrorAuthTypes,
FunctionErrorAuthTypes
> => {
await this.jobsManager.run("auth", "signOut", undefined);
try {
if (navigator.credentials && navigator.credentials.preventSilentAccess) {
await navigator.credentials.preventSilentAccess();
}
} catch (credentialsError) {
console.warn(BROWSER_NO_CREDENTIAL_API_MESSAGE, credentialsError);
}

return {
pending: false,
};
};

private autoSignIn = async () => {
let credentials;
try {
credentials = await (navigator.credentials as any).get({
orzechdev marked this conversation as resolved.
Show resolved Hide resolved
password: true,
});
} catch (credentialsError) {
console.warn(BROWSER_NO_CREDENTIAL_API_MESSAGE, credentialsError);
}

if (credentials) {
const { dataError } = await this.signIn(
credentials.id,
credentials.password,
true
);

if (dataError?.error) {
this.fireError(dataError.error, DataErrorAuthTypes.SIGN_IN);
}
}
};
}
5 changes: 5 additions & 0 deletions src/api/Auth/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum FunctionErrorAuthTypes {}
orzechdev marked this conversation as resolved.
Show resolved Hide resolved
export enum DataErrorAuthTypes {
"SIGN_IN",
"GET_USER",
}
Loading