Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Questions:

  •  Possibles questions about this feature

Technical Refinement

Description

  • SignUp

    • Create a feature flag:

      Code Block
      enable-required-acceptance-agreement-info
    • intl-user-service:

      • First, will add the new attribute on graphql:

        • path: intl-user-service/src/graphql/schema.graphql

          Code Block
          input SignUpUserInput {
            ...
            requiredAcceptanceAgreementInfo: [RequiredAcceptanceAgreementInfo!]
          }
          Code Block
          input RequiredAcceptanceAgreementInfo {
            id: String!
            updatedAt: String!
          }
        • After that, we will need to run the command to create graphql dependencies:

          Code Block
          yarn run generate:graphql-typings
        • On file: intl-user-service/src/graphql/graphql.ts, probably, will be many changes, but we can to discard all, except of change locally in the interface SignUpUserInput

          Code Block
          export interface SignUpUserInput {
            ...
            requiredAcceptanceAgreementInfo?: Nullable<RequiredAcceptanceAgreementInfo[]>;
          }
          
          export interface RequiredAcceptanceAgreementInfo {
            id: string;
            updatedAt: string;
          }
      • Now, we will add the new attribute on class SignupRequestDto:

        • path: intl-user-service/src/users/dtos/users.dtos.ts

          Code Block
          export class RequiredAcceptanceAgreementInfoDto {
            @ApiProperty({
              required: true,
              example: '0e0d12e4-d6cd-4b0e-bc9e-0cfbd590c922',
              description: 'Reference id by agreement required',
            })
            @IsString()
            public id: string;
          
            @ApiProperty({
              required: true,
              example: 'YYYY-MM-DDTHH:MM:SSZ',
              description: 'Update changed date',
            })
            @IsString()
            public updatedAt: string;
          
            constructor(id: string, updatedAt: string) {
              this.id = id;
              this.updatedAt = updatedAt;
            }
          }
          Code Block
          @ApiProperty({
            required: false,
            description: 'Required acceptance agreement info',
            type: [RequiredAcceptanceAgreementInfoDto],
            nullable: true,
          })
          @Type(() => RequiredAcceptanceAgreementInfoDto)
          @IsArray()
          @IsOptional()
          public requiredAcceptanceAgreementInfo?: RequiredAcceptanceAgreementInfoDto[] | null;
      • On file: intl-user-service/src/verification/utils.ts, we will need to create a mapper:

        Code Block
        /**
         * Map RequiredAcceptanceAgreementInfo[] to comply with RequiredAcceptanceAgreementInfoDto[]
         *
         * @param {RequiredAcceptanceAgreementInfo[] | null | undefined} acceptanceAgreementInfo
         * The RequiredAcceptanceAgreementInfo array.
         *
         * @returns {RequiredAcceptanceAgreementInfoDto[] | null}
         * The RequiredAcceptanceAgreementInfoDto array.
         */
        export const requiredAcceptanceAgreementInfoMapper = (
          acceptanceAgreementInfo: RequiredAcceptanceAgreementInfo[] | null | undefined,
        ): RequiredAcceptanceAgreementInfoDto[] | null => {
          if (!acceptanceAgreementInfo) return null;
          return acceptanceAgreementInfo.map((a) => {
            return new RequiredAcceptanceAgreementInfoDto(a.id, a.updatedAt);
          });
        };
      • We will add the requiredAcceptanceAgreementInfo in the call this.signupFacade.signUp:

        • path: intl-user-service/src/verification/verification.legacy-resolver.ts

          Code Block
          const result = await this.signupFacade.signUp(
                {
                  ...
                  connection: {
                    ...
                  },
                  ...
                  address: {
                    ...
                  },
                  ...
                  requiredAcceptanceAgreementInfo: requiredAcceptanceAgreementInfoMapper(
                    input.requiredAcceptanceAgreementInfo,
                  ),
                },
                ...
              );
      • We will need to add the requiredAcceptanceAgreementInfo of the destructuring from requestDto by method signUpUser:

        • path: intl-user-service/src/users/user-signup.service.ts

          Code Block
          const {
                ...
                requiredAcceptanceAgreementInfo,
              } = requestDto;
      • In the same method signUpUser, we will add the requiredAcceptanceAgreementInfo attribute signupCognito object:

        Code Block
        const signupCognito = {
          attributes: {
            ...
          },
          clientMetadata: {
            ...
          },
          ...
          requiredAcceptanceAgreementInfo: undefined,
        } as SignUpCognitoType;
        
        if (requiredAcceptanceAgreementInfo?.length) {
          signupCognito.requiredAcceptanceAgreementInfo = requiredAcceptanceAgreementInfo;
        }
      • And add the requiredAcceptanceDocumentInfo attribute on type SignUpCognitoType

        Code Block
        export type SignUpCognitoType = {
          attributes: {
            ...
          };
          clientMetadata: {
            ...
          };
          ...
          requiredAcceptanceAgreementInfo?: IRequiredAcceptanceAgreementInfo[];
        };
      • To this point, the cognito client will be call as method this.cognitoClient.signUp, to save the information in cognito (only attributes information), how our attribute is on clientMetadata , it won’t save in cognito, just on dynamodb. When the information is saved on cognito, will be call the PreSignupService through a gateway.

      • After that, we will need to add the requiredAcceptanceAgreementInfo attribute of the destructuring on method preSignupHandler

        • path: intl-user-service/src/cognito-lambdas/pre-signup.service.ts

          Code Block
          const { requiredAcceptanceAgreementInfo } = event.request || undefined;
        • And after, add the requiredAcceptanceAgreementInfo on this.usersRepository.create

          Code Block
          const resp = await this.usersRepository.create({
            ...
            requiredAcceptanceAgreementInfo,
          });
      • We will need to add too on interface ICreateUser:

        • path: intl-user-service/src/core/users.repository.ts

          Code Block
          export interface ICreateUser {
            ...
            requiredAcceptanceAgreementInfo?: IRequiredAcceptanceAgreementInfo[];
          }
          
          export interface IRequiredAcceptanceAgreementInfo {
            id: string;
            updatedAt: string;
          }
        • In the same file on method mapCreateUserToUserItem, we will add also the requiredAcceptanceAgreementInfo attribute:

          Code Block
          const {
                ...
                requiredAcceptanceAgreementInfo,
              } = createUser;
        • and on const userItem:

          Code Block
          const userItem: IUserItem = {
                ...
                details: {
                  ...
                  requiredAcceptanceAgreementInfo,
                },
                ...
              };
      • Now, will add the new attribute on IUserDetails interface:

        Code Block
        export interface IUserDetails {
          ...
          requiredAcceptanceAgreementInfo?: IRequiredAcceptanceAgreementInfo[] | null
        }
    • intl-packages:

    • Inside intl-packages/packages/users/, We will need to run the command to create intl-user-service dependencies:

      Code Block
      yarn run generate:openapi
    • We will need to add the requiredAcceptanceAgreementInfo attribute on interface IUserSignup

      • path: intl-packages/packages/users/src/schemata.ts

        Code Block
        export interface IUserSignup {
          ...
          requiredAcceptanceAgreementInfo?: IRequiredAcceptanceAgreementInfo[];
        }
        
        export interface IRequiredAcceptanceAgreementInfo {
          id: string;
          updatedAt: string;
        }
    • Also, we will add the requiredAcceptanceDocumentInfo of the destructuring from iUserSignup:

      • path: intl-packages/packages/users/src/services/user-service/user-service-client.ts

        Code Block
        const {
              ...
              requiredAcceptanceAgreementInfo,
            } = iUserSignup;
    • And we will include the requiredAcceptanceDocumentInfo as the parameter iSignupRequest to call signUpUser from intl-user-service

      Code Block
      const iSignupRequest = {
            ...
            address: {
              ...
            },
            ...
            requiredAcceptanceAgreementInfo: requiredAcceptanceAgreementInfo?.length
              ? requiredAcceptanceAgreementInfo
              : null,
            riskData: JSON.stringify({
              ...
            }),
            ...
          };
  • intl-whitelabel-graphql:

    • First, we need to update the intl-packages version, running the command:

      Code Block
      yarn run update-packages
    • Many packages will be updated, we can to discard all, except of change version "@rbilabs/users" locally intl-whitelabel-graphql/package.json

    • We will need to add a new attribute: requiredAcceptanceAgreementInfo: Boolean on input SignUpUserInput

      • path: intl-whitelabel-graphql/src/functions/graphql/schemas/users.gql

        Code Block
        input SignUpUserInput {
          ...
          requiredAcceptanceAgreementInfo: [RequiredAcceptanceAgreementInfo!]
        }
        
        input RequiredAcceptanceAgreementInfo {
          id: String!
          updatedAt: String!
        }
      • After that, we will need to run the command to create graphql dependencies:

        Code Block
        yarn run graphql:types
        • On file: intl-whitelabel-graphql/src/functions/graphql/generated/graphql.ts, probably, will be many changes, but we can to discard all, except of change locally in the interface ISignUpUserInput

          Code Block
          export interface SignUpUserInput {
            ....
            requiredAcceptanceAgreementInfo?: Maybe<RequiredAcceptanceAgreementInfo[]>;
          }
          
          export interface RequiredAcceptanceAgreementInfo {
            id: string;
          
            updatedAt: string;
          }
        • We will need to add too, the same attribute on interface IUserSignup

          • path: intl-whitelabel-graphql/src/functions/graphql/providers/users.ts

            Code Block
            export interface IUserSignup {
              ...
              requiredAcceptanceAgreementInfo?: IRequiredAcceptanceAgreementInfo[] | null
            }
        • On method signUpUser, we will add the requiredAcceptanceAgreementInfo of the destructuring from input:

          • path: intl-whitelabel-graphql/src/functions/graphql/providers/users.ts

            Code Block
            const {
                  ...
                  requiredAcceptanceAgreementInfo,
                } = input;
        • After that, we will need to attribute requiredAcceptanceAgreementInfo on the call method this.userClientSignUp

          Code Block
          const { userConfirmed, userId } = await this.userClientSignUp({
            ...
            riskData: JSON.stringify({
              ...
            }),
            ...
            address: {
              ...
            },
            ...
            requiredAcceptanceAgreementInfo: requiredAcceptanceAgreementInfo?.length
              ? requiredAcceptanceAgreementInfo
              : null,
          });
  • Sanity

    • rbi-whitelabel-cms:

      • We will add a new field:

        • path: rbi-whitelabel-cms/schemas/appData/documents/staticPage.tsx

          Code Block
          fields: [
              {
                title: 'Title',
                name: 'title',
                type: 'string',
              },
              {
                title: 'Requires User Acceptance',
                name: 'requiredUserAcceptance',
                type: 'boolean',
                hidden: () => !isIntl,
              },
              ...
          ]
  • intl-whitelabel-app:

    • We will need to run the command to create rbi-graphql dependencies:

      Code Block
      yarn run apollo:generate
      • On file: intl-whitelabel-app/workspaces/frontend/src/generated/rbi-graphql.tsx will be many changes, but we can to discard all, except of change locally in the interface ISignUpUserInput:

        Code Block
        export interface ISignUpUserInput {
          ...
          readonly requiredAcceptanceAgreementInfo?: Maybe<ReadonlyArray<IRequiredAcceptanceAgreementInfo>>,
        }
        
        export interface IRequiredAcceptanceAgreementInfo {
          readonly id: Scalars['String'],
          readonly updatedAt: Scalars['String'],
        }
  • Get Sanity _updatedAt

  • Now, we will create a new constant:

    • path: intl-whitelabel-app/workspaces/frontend/src/remote/queries/static-page.ts

      Code Block
      export const GetStaticPageAcceptanceAgreementQuery = gql`
        query GetStaticPageRoutes(
          $staticPageWhere: StaticPageFilter
        ) {
          allStaticPage (where: $staticPageWhere) {
            _id
            _updatedAt
            requiredUserAcceptance
            path {
              current
            }
            title
            localePath {
              ${Array.from(
                regionSupportedLanguages[getCountry().toUpperCase()] ||
                  Object.keys(PROD_SUPPORTED_LANGUAGES)
              ).map(lang => `${lang} { current }`)}
            }
          }
        }
      `;
      
    • We will change the parameters of the hook

      • path: intl-whitelabel-app/workspaces/frontend/src/state/static-page-manager/hooks/use-static-page-routes.ts

        • before

          Code Block
          useStaticPageRoutes = ()
        • after

          Code Block
          useStaticPageRoutes = (_queryStaticPage = GetStaticPageRoutesQuery)
      • And will change the call from GetStaticPageRoutesQuery to _queryStaticPage

        • before

          Code Block
          useLazySanityQuery<IGetStaticPageRoutesQuery>(GetStaticPageRoutesQuery, {}, { isV2: true });
        • after

          Code Block
          useLazySanityQuery<IGetStaticPageRoutesQuery>(_queryStaticPage, {}, { isV2: true });
  • We will need to get the _updatedAt from sanity, for it, we will create a function:

    • path: intl-whitelabel-app/workspaces/frontend/src/state/auth/hooks/use-account-authentication.ts

      Code Block
      const getRequiredAcceptanceAgreementInfo = useCallback(() => {
          if (!loadRoutesHasBeenCalled) {
            loadRoutes();
          }
          return staticPageRoute
            .map(pages => {
              if (pages?.requiredUserAcceptance) {
                return {
                  id: pages._id,
                  updatedAt: pages._updatedAt,
                };
              }
              return [];
            })
            .flat();
        }, [loadRoutes, loadRoutesHasBeenCalled, staticPageRoute]);
    • And import the hooks:

      Code Block
      const { loadRoutes, routes: staticPageRoute, loadRoutesHasBeenCalled } = useStaticPageRoutes(
        GetStaticPageAcceptanceAgreementQuery
      );
    • And add the useEffect:

      Code Block
      useEffectOnce(() => {
        loadRoutes();
      });
    • In the same file, we will need to call signUpMutation, on method signUp, sending the new attribute requiredAcceptanceAgreementInfo

      Code Block
      const requiredAcceptance = <FEATURE_FLAG> ? getRequiredAcceptanceAgreementInfo() : null;
      
      const { data } = await signUpMutation({
        variables: {
          input: {
            ...
            requiredAcceptanceAgreementInfo: {
              id: requiredAcceptance?._id,
              updatedAt: requiredAcceptance?._updatedAt,
            },
          },
        },
      });

Screenshots

TODO - images or links to images about this feature

POC

TODO - POC used to concept proof

Impact Analysis

All signup flow

Unit Test

Update the unit tests:

  • intl-whitelabel-app/workspaces/frontend/src/state/auth/hooks/use-account-authentication.test.ts

MOCKS

  • To mock:

    • path: intl-user-service/src/users/user-signup.service.ts

    • On constructor class SignupInitializer, we will add private readonly preSignupService: PreSignupService

      Code Block
      constructor(
          private readonly cognitoClient: CognitoClient,
          private readonly forterService: ForterService,
          private readonly preSignupService: PreSignupService,
        ) {}
    • And will replace all method signUpUser to:

      Code Block
      public async signUpUser(requestDto: SignupRequestDto): Promise<UserSignUpResult> {
          requestDto.username = getNormalizedEmail(requestDto.username);
          const {
            dob,
            name,
            phoneNumber,
            username,
            address,
            acceptedPromotionalEmails: hasAcceptedPromotionalEmails,
            gender,
            favoriteStores,
            providerType,
            acceptedPush: hasAcceptedPush,
            legacyId,
            requiredAcceptanceAgreementInfo,
          } = requestDto;
      
          /**
           * User service will not have region anymore so we will get it from the user
           * record
           * address->regionCode: IsoCountryCode
           */
          const region = ISO_ISO2_MAPPING[address.regionCode];
      
          if (!region) {
            return this.handleSignupError(undefined, address.regionCode, requestDto, {
              type: 'ValidationError',
            });
          }
      
          // there is no password but sdk requires a field to be passed
          const password = randomUUID();
      
          const response = await this.forterService.validateAccountSignup(region, requestDto);
      
          if (!response.isValid) {
            return this.handleSignupError(undefined, region, requestDto, {
              type: 'FraudValidationError',
            });
          }
      
          const signupCognito = {
            attributes: {
              birthdate: dob,
              locale: address.regionCode,
              name,
              // customAttributes must be predefined using nickname as a hack
              nickname: hasAcceptedPromotionalEmails ? 'true' : 'false',
              // eslint-disable-next-line @typescript-eslint/naming-convention
              phone_number: phoneNumber,
              gender,
            },
            clientMetadata: {
              zipcode: address.postalCode ?? '',
              favoriteStores: favoriteStores ? JSON.stringify(favoriteStores) : '',
            },
            password,
            username,
          } as SignUpCognitoType;
      
          if (providerType) {
            signupCognito.attributes[CustomCognitoAttribute.ProviderType] = providerType;
          }
      
          if (requiredAcceptanceAgreementInfo?.length) {
            signupCognito.requiredAcceptanceAgreementInfo = requiredAcceptanceAgreementInfo;
          }
      
          if (legacyId) {
            signupCognito.attributes[CustomCognitoAttribute.LegacyId] = legacyId;
          }
      
          signupCognito.attributes[CustomCognitoAttribute.EmailSubscribe] =
            (!!hasAcceptedPromotionalEmails).toString();
          signupCognito.attributes[CustomCognitoAttribute.PushSubscribe] = (!!hasAcceptedPush).toString();
      
          try {
            // logger.debug({ hashedEmail: hashString(username) }, 'Sending signup to cognito');
            // const { userConfirmed: wasUserConfirmed, userSub } = await this.cognitoClient.signUp(
            //   signupCognito,
            // );
      
            const event: ICognitoSignUpEvent = {
              userName: 'a2514415-36c9-4fe9-a396-f190030999f2',
              request: {
                userAttributes: {
                  ...signupCognito.attributes,
                  email: username,
                },
                clientMetadata: signupCognito.clientMetadata,
                validationData: {},
                requiredAcceptanceAgreementInfo: signupCognito.requiredAcceptanceAgreementInfo,
              },
              response: {
                autoConfirmUser: true,
                autoVerifyEmail: true,
              },
            };
      
            const result = await this.preSignupService.preSignupHandler(event);
      
            console.log('result: ', result);
      
            return this.handleSuccess(true, event.userName, region, requestDto);
          } catch (err) {
            return this.handleError(err, region, requestDto);
          }
        }
    • In the same file on class UserSignupService, we will change the constructor

      Code Block
      constructor(
          cognitoClient: CognitoClient,
          forterService: ForterService,
          jwtGeneration: JwtGeneration,
          userRepository: UsersRepository,
          preSignupService: PreSignupService,
        ) {
          this.signupInitializer = new SignupInitializer(cognitoClient, forterService, preSignupService);
          this.signupCompletion = new SignupCompletion(jwtGeneration, userRepository);
        }
    • On file intl-user-service/src/users/users.module.ts, we will add the service on @Module in the end file

      Code Block
      @Module({
        imports: [CoreModule, AppConfigModule, FraudModule],
        controllers: [UsersController, UserDeliveryAddressController],
        providers: [
          ...
          PreSignupService,
        ],
        exports: [UserService, UsersUpdateService, DeleteUserService, UserSignupService],
      })
    • After, we will call the signUp mutation from https://studio.apollographql.com/sandbox/explorer, just start the intl-user-service and call signUp

Workflows: https://lucid.app/lucidchart/56ec4afb-d8ea-42a2-a2dd-3cc38bfd87e7/edit?viewport_loc=-11%2C-75%2C2219%2C1004%2C0_0&invitationId=inv_57ecb6ad-cde2-4c13-be58-853204ed3007

...