@graphcommerce/magento-customer 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +294 -0
  2. package/components/AccountAddress/AccountAddress.gql.ts +4 -0
  3. package/components/AccountAddress/AccountAddress.graphql +6 -0
  4. package/components/AccountAddress/index.tsx +59 -0
  5. package/components/AccountAddresses/AccountAddresses.gql.ts +4 -0
  6. package/components/AccountAddresses/AccountAddresses.graphql +5 -0
  7. package/components/AccountAddresses/UpdateDefaultAddress.gql.ts +14 -0
  8. package/components/AccountAddresses/UpdateDefaultAddress.graphql +14 -0
  9. package/components/AccountAddresses/index.tsx +101 -0
  10. package/components/AddressFields/index.tsx +189 -0
  11. package/components/AddressMultiLine/index.tsx +68 -0
  12. package/components/AddressSingleLine/index.tsx +34 -0
  13. package/components/ApolloCustomerError/ApolloCustomerErrorAlert.tsx +21 -0
  14. package/components/ApolloCustomerError/ApolloCustomerErrorFullPage.tsx +46 -0
  15. package/components/ChangeNameForm/UpdateCustomerName.gql.ts +14 -0
  16. package/components/ChangeNameForm/UpdateCustomerName.graphql +9 -0
  17. package/components/ChangeNameForm/index.tsx +66 -0
  18. package/components/ChangePasswordForm/ChangePassword.gql.ts +13 -0
  19. package/components/ChangePasswordForm/ChangePassword.graphql +5 -0
  20. package/components/ChangePasswordForm/ChangePasswordForm.tsx +93 -0
  21. package/components/CreateCustomerAddressForm/CreateCustomerAddress.gql.ts +28 -0
  22. package/components/CreateCustomerAddressForm/CreateCustomerAddress.graphql +41 -0
  23. package/components/CreateCustomerAddressForm/CustomerAddress.gql.ts +4 -0
  24. package/components/CreateCustomerAddressForm/CustomerAddress.graphql +22 -0
  25. package/components/CreateCustomerAddressForm/CustomerAddressEdit.gql.ts +4 -0
  26. package/components/CreateCustomerAddressForm/CustomerAddressEdit.graphql +4 -0
  27. package/components/CreateCustomerAddressForm/index.tsx +98 -0
  28. package/components/CustomerFab/index.tsx +55 -0
  29. package/components/CustomerMenuFabItem/index.tsx +68 -0
  30. package/components/DeleteCustomerAddressForm/DeleteCustomerAddressForm.gql.ts +12 -0
  31. package/components/DeleteCustomerAddressForm/DeleteCustomerAddressForm.graphql +3 -0
  32. package/components/DeleteCustomerAddressForm/index.tsx +46 -0
  33. package/components/EditAddressForm/UpdateCustomerAddress.gql.ts +29 -0
  34. package/components/EditAddressForm/UpdateCustomerAddress.graphql +43 -0
  35. package/components/EditAddressForm/index.tsx +127 -0
  36. package/components/ForgotPasswordForm/ForgotPassword.gql.ts +12 -0
  37. package/components/ForgotPasswordForm/ForgotPassword.graphql +3 -0
  38. package/components/ForgotPasswordForm/ForgotPasswordForm.tsx +73 -0
  39. package/components/InlineAccount/InlineAccount.gql.ts +12 -0
  40. package/components/InlineAccount/InlineAccount.graphql +10 -0
  41. package/components/InlineAccount/index.tsx +149 -0
  42. package/components/NameFields/index.tsx +89 -0
  43. package/components/ResetPasswordForm/ResetPassword.gql.ts +14 -0
  44. package/components/ResetPasswordForm/ResetPassword.graphql +3 -0
  45. package/components/ResetPasswordForm/index.tsx +98 -0
  46. package/components/SignInForm/SignIn.gql.ts +13 -0
  47. package/components/SignInForm/SignIn.graphql +5 -0
  48. package/components/SignInForm/SignInForm.tsx +96 -0
  49. package/components/SignInForm/SignInFormInline.tsx +68 -0
  50. package/components/SignOutForm/SignOutForm.gql.ts +10 -0
  51. package/components/SignOutForm/SignOutForm.graphql +5 -0
  52. package/components/SignOutForm/index.tsx +30 -0
  53. package/components/SignUpForm/SignUp.gql.ts +22 -0
  54. package/components/SignUpForm/SignUp.graphql +36 -0
  55. package/components/SignUpForm/SignUpForm.tsx +90 -0
  56. package/components/SignUpForm/SignUpFormInline.tsx +113 -0
  57. package/components/UpdateCustomerEmailForm/UpdateCustomerEmail.gql.ts +13 -0
  58. package/components/UpdateCustomerEmailForm/UpdateCustomerEmail.graphql +7 -0
  59. package/components/UpdateCustomerEmailForm/index.tsx +133 -0
  60. package/components/UpdateDefaultAddressForm/index.tsx +91 -0
  61. package/components/index.ts +29 -0
  62. package/hooks/Customer.gql.ts +10 -0
  63. package/hooks/Customer.graphql +5 -0
  64. package/hooks/CustomerInfo.gql.ts +4 -0
  65. package/hooks/CustomerInfo.graphql +20 -0
  66. package/hooks/CustomerToken.gql.ts +10 -0
  67. package/hooks/CustomerToken.graphql +8 -0
  68. package/hooks/CustomerToken.graphqls +8 -0
  69. package/hooks/IsEmailAvailable.gql.ts +12 -0
  70. package/hooks/IsEmailAvailable.graphql +5 -0
  71. package/hooks/index.ts +7 -0
  72. package/hooks/useExtractCustomerErrors.tsx +42 -0
  73. package/hooks/useFormIsEmailAvailable.tsx +69 -0
  74. package/index.ts +4 -0
  75. package/next-env.d.ts +4 -0
  76. package/package.json +41 -0
  77. package/tsconfig.json +5 -0
  78. package/typePolicies.ts +91 -0
@@ -0,0 +1,10 @@
1
+ /* eslint-disable */
2
+ import * as Types from '@graphcommerce/graphql';
3
+
4
+ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
5
+
6
+ export const CustomerTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CustomerToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customerToken"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"client"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<CustomerTokenQuery, CustomerTokenQueryVariables>;
7
+ export type CustomerTokenQueryVariables = Types.Exact<{ [key: string]: never; }>;
8
+
9
+
10
+ export type CustomerTokenQuery = { customerToken?: Types.Maybe<{ __typename: 'CustomerToken', token?: Types.Maybe<string>, valid?: Types.Maybe<boolean>, createdAt?: Types.Maybe<string> }> };
@@ -0,0 +1,8 @@
1
+ query CustomerToken {
2
+ customerToken @client {
3
+ __typename
4
+ token
5
+ valid
6
+ createdAt
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ extend type Query {
2
+ customerToken: CustomerToken
3
+ }
4
+
5
+ extend type CustomerToken {
6
+ createdAt: String
7
+ valid: Boolean
8
+ }
@@ -0,0 +1,12 @@
1
+ /* eslint-disable */
2
+ import * as Types from '@graphcommerce/graphql';
3
+
4
+ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
5
+
6
+ export const IsEmailAvailableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"IsEmailAvailable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isEmailAvailable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"is_email_available"}}]}}]}}]} as unknown as DocumentNode<IsEmailAvailableQuery, IsEmailAvailableQueryVariables>;
7
+ export type IsEmailAvailableQueryVariables = Types.Exact<{
8
+ email: Types.Scalars['String'];
9
+ }>;
10
+
11
+
12
+ export type IsEmailAvailableQuery = { isEmailAvailable?: Types.Maybe<{ is_email_available?: Types.Maybe<boolean> }> };
@@ -0,0 +1,5 @@
1
+ query IsEmailAvailable($email: String!) {
2
+ isEmailAvailable(email: $email) {
3
+ is_email_available
4
+ }
5
+ }
package/hooks/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { default as useFormIsEmailAvailable } from './useFormIsEmailAvailable'
2
+ export * from './useExtractCustomerErrors'
3
+
4
+ export * from './Customer.gql'
5
+ export * from './CustomerToken.gql'
6
+ export * from './CustomerInfo.gql'
7
+ export * from './IsEmailAvailable.gql'
@@ -0,0 +1,42 @@
1
+ import { ApolloError, useApolloClient } from '@apollo/client'
2
+ import { graphqlErrorByCategory } from '@graphcommerce/magento-graphql'
3
+ import { useEffect } from 'react'
4
+ import { CustomerTokenDocument } from './CustomerToken.gql'
5
+
6
+ export type UseExtractErrors = { error?: ApolloError }
7
+ export function useExtractCustomerErrors({ error }: UseExtractErrors) {
8
+ const client = useApolloClient()
9
+
10
+ const [newError, unauthorized] = graphqlErrorByCategory({
11
+ category: 'graphql-authorization',
12
+ error,
13
+ extract: false,
14
+ mask: 'You need to login to continue',
15
+ })
16
+
17
+ useEffect(() => {
18
+ if (!unauthorized) return
19
+
20
+ const { customerToken } =
21
+ client.cache.readQuery({
22
+ query: CustomerTokenDocument,
23
+ }) ?? {}
24
+
25
+ if (customerToken) {
26
+ // Write arbitrary old token to document
27
+ client.cache.writeQuery({
28
+ query: CustomerTokenDocument,
29
+ data: {
30
+ customerToken: {
31
+ ...customerToken,
32
+ createdAt: new Date('2000').toUTCString(),
33
+ valid: false,
34
+ },
35
+ },
36
+ broadcast: true,
37
+ })
38
+ }
39
+ }, [client.cache, unauthorized])
40
+
41
+ return { error: newError, unauthorized }
42
+ }
@@ -0,0 +1,69 @@
1
+ import { useQuery } from '@apollo/client'
2
+ import { useFormAutoSubmit, useFormGqlQuery, useFormPersist } from '@graphcommerce/react-hook-form'
3
+ import { useEffect, useState } from 'react'
4
+ import { CustomerDocument } from './Customer.gql'
5
+ import { CustomerTokenDocument } from './CustomerToken.gql'
6
+ import { IsEmailAvailableDocument } from './IsEmailAvailable.gql'
7
+
8
+ type useFormIsEmailAvailableProps = {
9
+ email?: string | null
10
+ onSubmitted?: (data: { email: string }) => void
11
+ }
12
+
13
+ export default function useFormIsEmailAvailable(props: useFormIsEmailAvailableProps) {
14
+ const { email, onSubmitted } = props
15
+ const { data: token } = useQuery(CustomerTokenDocument)
16
+ const customerQuery = useQuery(CustomerDocument, {
17
+ ssr: false,
18
+ skip: typeof token === 'undefined',
19
+ })
20
+
21
+ const form = useFormGqlQuery(IsEmailAvailableDocument, {
22
+ mode: 'onChange',
23
+ defaultValues: { email: email ?? '' },
24
+ })
25
+ const { formState, data, handleSubmit } = form
26
+
27
+ const submit = handleSubmit(onSubmitted || (() => {}))
28
+ const autoSubmitting = useFormAutoSubmit({ form, submit })
29
+
30
+ const hasAccount = data?.isEmailAvailable?.is_email_available === false
31
+ const { isDirty, isSubmitSuccessful, isSubmitted, isSubmitting, isValid } = formState
32
+
33
+ const isLoggedIn = token?.customerToken && token?.customerToken.valid
34
+
35
+ const [mode, setMode] = useState<'email' | 'signin' | 'signup' | 'signedin' | 'session-expired'>(
36
+ token?.customerToken && token?.customerToken.valid ? 'signedin' : 'email',
37
+ )
38
+
39
+ useFormPersist({ form, name: 'IsEmailAvailable' })
40
+
41
+ useEffect(() => {
42
+ if (isLoggedIn) {
43
+ setMode('signedin')
44
+ return
45
+ }
46
+ if (isSubmitting) return
47
+ if (!isValid) {
48
+ setMode('email')
49
+ return
50
+ }
51
+ if (!isDirty && isSubmitted && isSubmitSuccessful && isValid)
52
+ setMode(hasAccount ? 'signin' : 'signup')
53
+
54
+ if (customerQuery.data?.customer && token && token.customerToken && !token.customerToken.valid)
55
+ setMode(isSubmitSuccessful ? 'signin' : 'session-expired')
56
+ }, [
57
+ customerQuery.data?.customer,
58
+ hasAccount,
59
+ isDirty,
60
+ isLoggedIn,
61
+ isSubmitSuccessful,
62
+ isSubmitted,
63
+ isSubmitting,
64
+ isValid,
65
+ token,
66
+ ])
67
+
68
+ return { mode, form, token, submit, autoSubmitting, hasAccount }
69
+ }
package/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './components'
2
+ export * from './hooks'
3
+
4
+ export * from './typePolicies'
package/next-env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/types/global" />
3
+ /// <reference types="next/image-types/global" />
4
+ /// <reference types="@graphcommerce/next-ui/types" />
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@graphcommerce/magento-customer",
3
+ "version": "3.0.1",
4
+ "sideEffects": false,
5
+ "prettier": "@graphcommerce/prettier-config-pwa",
6
+ "browserslist": [
7
+ "extends @graphcommerce/browserslist-config-pwa"
8
+ ],
9
+ "eslintConfig": {
10
+ "extends": "@graphcommerce/eslint-config-pwa",
11
+ "parserOptions": {
12
+ "project": "./tsconfig.json"
13
+ }
14
+ },
15
+ "devDependencies": {
16
+ "@graphcommerce/browserslist-config-pwa": "^3.0.1",
17
+ "@graphcommerce/eslint-config-pwa": "^3.0.1",
18
+ "@graphcommerce/prettier-config-pwa": "^3.0.1",
19
+ "@graphcommerce/typescript-config-pwa": "^3.0.1",
20
+ "@playwright/test": "^1.14.1"
21
+ },
22
+ "dependencies": {
23
+ "@apollo/client": "^3.3.21",
24
+ "@graphcommerce/graphql": "^2.103.1",
25
+ "@graphcommerce/image": "^2.104.1",
26
+ "@graphcommerce/magento-cart": "^3.0.1",
27
+ "@graphcommerce/magento-graphql": "^2.103.1",
28
+ "@graphcommerce/magento-product": "^3.0.1",
29
+ "@graphcommerce/magento-store": "^3.0.1",
30
+ "@graphcommerce/next-ui": "^3.0.1",
31
+ "@graphcommerce/react-hook-form": "^2.102.1",
32
+ "@graphql-typed-document-node/core": "^3.1.0",
33
+ "@material-ui/core": "^4.12.3",
34
+ "@material-ui/lab": "^4.0.0-alpha.60",
35
+ "clsx": "^1.1.1",
36
+ "framer-motion": "^4.1.17",
37
+ "next": "^11.1.2",
38
+ "react": "^17.0.2",
39
+ "react-dom": "^17.0.2"
40
+ }
41
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "exclude": ["**/node_modules", "**/.*/"],
3
+ "include": ["**/*.ts", "**/*.tsx"],
4
+ "extends": "@graphcommerce/typescript-config-pwa/nextjs.json"
5
+ }
@@ -0,0 +1,91 @@
1
+ import { FieldPolicy, FieldReadFunction } from '@apollo/client'
2
+ import {
3
+ CustomerToken,
4
+ MigrateCache,
5
+ Mutation,
6
+ Query,
7
+ TypedTypePolicies,
8
+ } from '@graphcommerce/graphql'
9
+ import { CustomerTokenDocument } from './hooks/CustomerToken.gql'
10
+ import { IsEmailAvailableDocument } from './hooks/IsEmailAvailable.gql'
11
+
12
+ const revokeCustomerToken: FieldPolicy<Mutation['revokeCustomerToken']> = {
13
+ merge(_existing, incoming, options) {
14
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
15
+ options.cache.reset()
16
+ return incoming
17
+ },
18
+ }
19
+
20
+ const TOKEN_EXPIRATION_MS = 60 * 60 * 1000
21
+
22
+ const valid: FieldPolicy<CustomerToken['valid']> = {
23
+ read(existing, options) {
24
+ if (existing === undefined) return existing
25
+
26
+ const ref = options.toReference({ __ref: 'CustomerToken' })
27
+ const createdAt = options.readField<string>('createdAt', ref)
28
+
29
+ if (!createdAt) return existing
30
+
31
+ return new Date().getTime() - new Date(createdAt).getTime() < TOKEN_EXPIRATION_MS
32
+ },
33
+ }
34
+
35
+ const generateCustomerToken: FieldPolicy<Mutation['generateCustomerToken']> = {
36
+ keyArgs: () => '',
37
+ merge(_existing, incoming, options) {
38
+ if (!options.isReference(incoming)) return incoming
39
+
40
+ const write = () => {
41
+ options.cache.writeQuery({
42
+ query: CustomerTokenDocument,
43
+ broadcast: true,
44
+ data: {
45
+ customerToken: {
46
+ __typename: 'CustomerToken',
47
+ token: options.readField('token', incoming) as string,
48
+ createdAt: new Date().toUTCString(),
49
+ valid: true,
50
+ },
51
+ },
52
+ })
53
+ }
54
+ write()
55
+
56
+ // Broadcasts the query after the token expiration so UI gets updated
57
+ setTimeout(write, TOKEN_EXPIRATION_MS)
58
+ return incoming
59
+ },
60
+ }
61
+
62
+ const createCustomer: FieldPolicy<Mutation['createCustomer']> = {
63
+ merge(_existing, incoming, options) {
64
+ if (incoming?.customer.email) {
65
+ options.cache.writeQuery({
66
+ query: IsEmailAvailableDocument,
67
+ variables: { email: incoming?.customer.email },
68
+ data: { isEmailAvailable: { is_email_available: false } },
69
+ broadcast: true,
70
+ })
71
+ }
72
+
73
+ return incoming
74
+ },
75
+ }
76
+
77
+ // const customer: FieldReadFunction<Query['customer']> = (incoming, options) => {
78
+ // if (!options.canRead(incoming)) return null
79
+ // return incoming
80
+ // }
81
+
82
+ export const customerTypePolicies: TypedTypePolicies = {
83
+ // Query: { fields: { customer } },
84
+ Mutation: { fields: { generateCustomerToken, revokeCustomerToken, createCustomer } },
85
+ CustomerToken: { fields: { valid } },
86
+ }
87
+
88
+ export const migrateCustomer: MigrateCache = (oldCache, newCache) => {
89
+ const token = oldCache.readQuery({ query: CustomerTokenDocument })
90
+ newCache.writeQuery({ query: CustomerTokenDocument, data: token, broadcast: true })
91
+ }