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 interfaceSignUpUserInput
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 @ApiProperty({ required: false, description: 'Required acceptance agreement info', nullable: true, }) @Type(() => RequiredAcceptanceAgreementInfoDto) @IsArray() @IsOptional() public requiredAcceptanceAgreementInfo?: RequiredAcceptanceAgreementInfoDto[] | null;
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; } }() 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 callthis.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 fromrequestDto
by methodsignUpUser
:path:
intl-user-service/src/users/user-signup.service.ts
Code Block const { ... requiredAcceptanceAgreementInfo, } = requestDto;
In the same method
signUpUser
, we will add therequiredAcceptanceAgreementInfo
attributesignupCognito
object:Code Block const signupCognito = { attributes: { ... }, clientMetadata: { ... }, ... requiredAcceptanceAgreementInfo: undefined, } as SignUpCognitoType; if (requiredAcceptanceAgreementInfo?.length) { signupCognito.requiredAcceptanceAgreementInfo = requiredAcceptanceAgreementInfo; }
And add the
requiredAcceptanceDocumentInfo
attribute on typeSignUpCognitoType
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 (onlyattributes
information), how our attribute is onclientMetadata
, it won’t save in cognito, just on dynamodb. When the information is saved on cognito, will be call thePreSignupService
through a gateway.In this step, we won’t be able to test 'cause the gateway call. We will need to mock some data to test: https://rbictg.atlassian.net/wiki/spaces/IN/pages/4040164203/Terms+and+Conditions+-+Technical+Refinement#MOCKS
After that, we will need to add the
requiredAcceptanceAgreementInfo
attribute of the destructuring on methodpreSignupHandler
path:
intl-user-service/src/cognito-lambdas/pre-signup.service.ts
Code Block const { requiredAcceptanceAgreementInfo } = event.request || undefined;
And after, add the
requiredAcceptanceAgreementInfo
onthis.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 therequiredAcceptanceAgreementInfo
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 createintl-user-service
dependencies:Code Block yarn run generate:openapi
We will need to add the
requiredAcceptanceAgreementInfo
attribute on interfaceIUserSignup
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 fromiUserSignup
: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 parameteriSignupRequest
to callsignUpUser
fromintl-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"
locallyintl-whitelabel-graphql/package.json
We will need to add a new attribute:
requiredAcceptanceAgreementInfo: Boolean
on inputSignUpUserInput
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 interfaceISignUpUserInput
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 therequiredAcceptanceAgreementInfo
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 methodthis.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 interfaceISignUpUserInput
: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 methodsignUp
, sending the new attributerequiredAcceptanceAgreementInfo
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
classSignupInitializer
, we will addprivate 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 theconstructor
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 fileCode 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 callsignUp
Useful Links
...