diff --git a/services/identity/src/main/java/com/crapi/constant/UserMessage.java b/services/identity/src/main/java/com/crapi/constant/UserMessage.java index ad0c6545..5c5beb93 100644 --- a/services/identity/src/main/java/com/crapi/constant/UserMessage.java +++ b/services/identity/src/main/java/com/crapi/constant/UserMessage.java @@ -34,6 +34,14 @@ public class UserMessage { "User registered successfully! Please Login."; public static final String SIGN_UP_FAILED = "User registered failed! Please retry."; public static final String NUMBER_ALREADY_REGISTERED = "Number already registered! Number: "; + public static final String NUMBER_NOT_REGISTERED = "Given Number is not registered! Number:"; + public static final String CHANGE_PHONE_MESSAGE = + "The otp has been sent to your email. If you have used example.com email, check your email using the MailHog web portal."; + public static final String NUMBER_CHANGE_SUCCESSFUL = "Phone number change is successful"; + public static final String NEW_NUMBER_DOES_NOT_BELONG = + "Fail, new number parameter doesn’t match with OTP"; + public static final String OLD_NUMBER_DOES_NOT_BELONG = + "Fail, number parameter doesn’t belong to the user"; public static final String EMAIL_ALREADY_REGISTERED = "Email already registered! Email: "; public static final String GIVEN_URL_ALREADY_USED = "Given URL is already used! Please try to login.."; diff --git a/services/identity/src/main/java/com/crapi/controller/ChangePhoneController.java b/services/identity/src/main/java/com/crapi/controller/ChangePhoneController.java new file mode 100644 index 00000000..f5009c30 --- /dev/null +++ b/services/identity/src/main/java/com/crapi/controller/ChangePhoneController.java @@ -0,0 +1,55 @@ +package com.crapi.controller; + +import com.crapi.model.CRAPIResponse; +import com.crapi.model.ChangePhoneForm; +import com.crapi.service.UserService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@CrossOrigin +@RestController +@RequestMapping("/identity/api") +public class ChangePhoneController { + @Autowired UserService userService; + + /** + * @param changePhoneForm changePhoneForm contains old phone number and new phone number, api will + * send otp to email address. + * @param request getting jwt token for user from request header + * @return first check phone number is already registered or not if it is there then return phone + * number already registered then try with new phone number. + */ + @PostMapping("/v2/user/change-phone-number") + public ResponseEntity changesPhone( + @Valid @RequestBody ChangePhoneForm changePhoneForm, HttpServletRequest request) { + CRAPIResponse changePhoneResponse = userService.changePhoneRequest(request, changePhoneForm); + if (changePhoneResponse != null && changePhoneResponse.getStatus() == 403) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(changePhoneResponse); + } else if (changePhoneResponse != null && changePhoneResponse.getStatus() == 404) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(changePhoneResponse); + } + return ResponseEntity.status(HttpStatus.OK).body(changePhoneResponse); + } + + /** + * @param changePhoneForm changeEmailForm contains old phone number and new phone number, with + * otp, this function will verify number and otp + * @param request getting jwt token for user from request header + * @return verify if otp is valid then it will update the user phone number + */ + @PostMapping("v2/user/verify-phone-otp") + public ResponseEntity verifyPhoneOTP( + @RequestBody ChangePhoneForm changePhoneForm, HttpServletRequest request) { + CRAPIResponse verifyPhoneOTPResponse = userService.verifyPhoneOTP(request, changePhoneForm); + if (verifyPhoneOTPResponse != null && verifyPhoneOTPResponse.getStatus() == 200) { + return ResponseEntity.status(HttpStatus.OK).body(verifyPhoneOTPResponse); + } else if (verifyPhoneOTPResponse != null && verifyPhoneOTPResponse.getStatus() == 404) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(verifyPhoneOTPResponse); + } + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(verifyPhoneOTPResponse); + } +} diff --git a/services/identity/src/main/java/com/crapi/entity/ChangePhoneRequest.java b/services/identity/src/main/java/com/crapi/entity/ChangePhoneRequest.java new file mode 100644 index 00000000..e7792102 --- /dev/null +++ b/services/identity/src/main/java/com/crapi/entity/ChangePhoneRequest.java @@ -0,0 +1,37 @@ +package com.crapi.entity; + +import com.crapi.enums.EStatus; +import jakarta.persistence.*; +import lombok.Data; + +@Entity +@Table(name = "otp_phoneNumberChange") +@Data +public class ChangePhoneRequest { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + @Column(name = "new_phone") + private String newPhone; + + @Column(name = "old_phone") + private String oldPhone; + + @Column(name = "otp") + private String otp; + + private String status; + + @OneToOne private User user; + + public ChangePhoneRequest() {} + + public ChangePhoneRequest(String newPhone, String oldPhone, String otp, User user) { + this.newPhone = newPhone; + this.oldPhone = oldPhone; + this.otp = otp; + this.user = user; + this.status = EStatus.ACTIVE.toString(); + } +} diff --git a/services/identity/src/main/java/com/crapi/model/ChangePhoneForm.java b/services/identity/src/main/java/com/crapi/model/ChangePhoneForm.java new file mode 100644 index 00000000..9dece03a --- /dev/null +++ b/services/identity/src/main/java/com/crapi/model/ChangePhoneForm.java @@ -0,0 +1,19 @@ +package com.crapi.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class ChangePhoneForm { + @NotBlank + @Size(max = 15) + private String old_number; + + @NotBlank + @Size(max = 15) + private String new_number; + + @Size(min = 3, max = 4) + private String otp; +} diff --git a/services/identity/src/main/java/com/crapi/repository/ChangePhoneRepository.java b/services/identity/src/main/java/com/crapi/repository/ChangePhoneRepository.java new file mode 100644 index 00000000..b4ddd77e --- /dev/null +++ b/services/identity/src/main/java/com/crapi/repository/ChangePhoneRepository.java @@ -0,0 +1,11 @@ +package com.crapi.repository; + +import com.crapi.entity.ChangePhoneRequest; +import com.crapi.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ChangePhoneRepository extends JpaRepository { + ChangePhoneRequest findByUser(User user); +} diff --git a/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java b/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java index 4b848481..4324db38 100644 --- a/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java +++ b/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java @@ -72,6 +72,8 @@ public class UserServiceImpl implements UserService { @Autowired AuthenticationManager authenticationManager; + @Autowired ChangePhoneRepository changePhoneRepository; + public UserServiceImpl() { setFactory(log4jContextFactory); LOG4J_LOGGER = LogManager.getLogger(UserService.class); @@ -471,4 +473,86 @@ public ApiKeyResponse generateApiKey(HttpServletRequest request) { } return new ApiKeyResponse(""); } + + /** + * @param changePhoneForm contains old phone number and new phone number, api will send otp to + * change number to email address. + * @return send otp to email with random generated otp. + */ + @Transactional + @Override + public CRAPIResponse changePhoneRequest( + HttpServletRequest request, ChangePhoneForm changePhoneForm) { + String otp; + User user; + ChangePhoneRequest changePhoneRequest; + // checking if new phone in user login table if present then disallow + if (userRepository.existsByNumber(changePhoneForm.getNew_number())) { + return new CRAPIResponse( + UserMessage.NUMBER_ALREADY_REGISTERED + changePhoneForm.getNew_number(), 403); + } + // checking if old phone is registered or not + if (!userRepository.existsByNumber(changePhoneForm.getOld_number())) { + return new CRAPIResponse( + (UserMessage.NUMBER_NOT_REGISTERED) + changePhoneForm.getOld_number(), 404); + } + + otp = OTPGenerator.generateRandom(4); + user = getUserFromToken(request); + // fetching change phone data for user + changePhoneRequest = changePhoneRepository.findByUser(user); + if (changePhoneRequest == null) { + // Creating new object if changePhone data for user in not in database + changePhoneRequest = + new ChangePhoneRequest( + changePhoneForm.getNew_number(), changePhoneForm.getOld_number(), otp, user); + } else { + // updating existing record + changePhoneRequest.setOtp(otp); + changePhoneRequest.setOldPhone(changePhoneForm.getOld_number()); + changePhoneRequest.setNewPhone(changePhoneForm.getNew_number()); + } + changePhoneForm.setOtp(otp); + changePhoneRepository.save(changePhoneRequest); + smtpMailServer.sendMail( + user.getEmail(), + MailBody.changeMailBody(changePhoneForm), + "crAPI: Change Phone Number OTP"); + + return new CRAPIResponse( + UserMessage.CHANGE_PHONE_MESSAGE + changePhoneForm.getNew_number(), 200); + } + + /** + * @param request getting jwt token for user from request header + * @param changePhoneForm contains old phone number and new phone number, with otp, this function + * will verify phone number and otp + * @return it checks user token and verify with otp if user verify then correct then we will + * update email for user. + */ + @Transactional + @Override + public CRAPIResponse verifyPhoneOTP(HttpServletRequest request, ChangePhoneForm changePhoneForm) { + ChangePhoneRequest changePhoneRequest; + User user; + user = getUserFromToken(request); + changePhoneRequest = changePhoneRepository.findByUser(user); + if (changePhoneRequest != null) { + if (changePhoneForm.getOtp() != null + && changePhoneForm.getOtp().equalsIgnoreCase(changePhoneRequest.getOtp())) { + if (changePhoneForm.getOld_number().equalsIgnoreCase((user.getNumber()))) { + if (changePhoneForm.getNew_number().equalsIgnoreCase(changePhoneRequest.getNewPhone())) { + user.setNumber(changePhoneRequest.getNewPhone()); + userRepository.save(user); + return new CRAPIResponse(UserMessage.NUMBER_CHANGE_SUCCESSFUL, 200); + } + return new CRAPIResponse(UserMessage.NEW_NUMBER_DOES_NOT_BELONG, 500); + } + return new CRAPIResponse(UserMessage.OLD_NUMBER_DOES_NOT_BELONG, 500); + } + return new CRAPIResponse(UserMessage.INVALID_OTP, 500); + } + + return new CRAPIResponse(UserMessage.INVALID_CREDENTIALS, 500); + } } diff --git a/services/identity/src/main/java/com/crapi/service/UserService.java b/services/identity/src/main/java/com/crapi/service/UserService.java index 9c49d087..2a93f151 100644 --- a/services/identity/src/main/java/com/crapi/service/UserService.java +++ b/services/identity/src/main/java/com/crapi/service/UserService.java @@ -35,8 +35,12 @@ CRAPIResponse resetPassword(LoginForm loginForm, HttpServletRequest request) CRAPIResponse changeEmailRequest(HttpServletRequest request, ChangeEmailForm loginForm); + CRAPIResponse changePhoneRequest(HttpServletRequest request, ChangePhoneForm changePhoneForm); + CRAPIResponse verifyEmailToken(HttpServletRequest request, ChangeEmailForm changeEmailForm); + CRAPIResponse verifyPhoneOTP(HttpServletRequest request, ChangePhoneForm changePhoneForm); + User getUserFromToken(HttpServletRequest request); User getUserFromTokenWithoutValidation(HttpServletRequest request); diff --git a/services/identity/src/main/java/com/crapi/utils/MailBody.java b/services/identity/src/main/java/com/crapi/utils/MailBody.java index 5742f7ed..28090f5c 100644 --- a/services/identity/src/main/java/com/crapi/utils/MailBody.java +++ b/services/identity/src/main/java/com/crapi/utils/MailBody.java @@ -18,6 +18,7 @@ import com.crapi.entity.UserDetails; import com.crapi.entity.VehicleDetails; import com.crapi.model.ChangeEmailForm; +import com.crapi.model.ChangePhoneForm; public class MailBody { @@ -74,7 +75,7 @@ public static String signupMailBody(VehicleDetails vehicleDetails, String name) /** * @param changeEmailRequest - * @return Mail Body, for Chnage Email. + * @return Mail Body, for Change Email. */ public static String changeMailBody(ChangeEmailForm changeEmailRequest) { String msgBody = @@ -103,6 +104,37 @@ public static String changeMailBody(ChangeEmailForm changeEmailRequest) { return msgBody; } + /** + * @param changePhoneRequest + * @return Mail Body, for Change Phone number. + */ + public static String changeMailBody(ChangePhoneForm changePhoneRequest) { + String msgBody = + "" + + "Hi" + + "," + + "

We received a request to change your account phone Number. The previous number is: " + + changePhoneRequest.getOld_number() + + "" + + " and the new one is: " + + changePhoneRequest.getNew_number() + + "

" + + "To complete the process, please use the following otp: " + + changePhoneRequest.getOtp() + + "" + + "
" + + "
" + + "

If you haven not sent a request to change your phone number, please ignore this message.

" + + "

Thank You & have a wonderful day !

" + + "Warm Regards,
crAPI - Team

" + + "Email: support@crapi.io

  
" + + "This E-mail and any attachments are private, intended solely for the use of the addressee. If you are not the intended recipient, they have been sent to you in error: any use of information in them is strictly prohibited. " + + "" + + ""; + + return msgBody; + } + /** * @param code * @param email diff --git a/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java b/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java index 7a9b3471..b7fadcb4 100644 --- a/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java +++ b/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java @@ -20,6 +20,7 @@ import com.crapi.config.JwtProvider; import com.crapi.constant.UserMessage; import com.crapi.entity.ChangeEmailRequest; +import com.crapi.entity.ChangePhoneRequest; import com.crapi.entity.ProfileVideo; import com.crapi.entity.User; import com.crapi.entity.UserDetails; @@ -27,12 +28,14 @@ import com.crapi.exception.EntityNotFoundException; import com.crapi.model.CRAPIResponse; import com.crapi.model.ChangeEmailForm; +import com.crapi.model.ChangePhoneForm; import com.crapi.model.DashboardResponse; import com.crapi.model.JwtResponse; import com.crapi.model.LoginForm; import com.crapi.model.LoginWithEmailToken; import com.crapi.model.SignUpForm; import com.crapi.repository.ChangeEmailRepository; +import com.crapi.repository.ChangePhoneRepository; import com.crapi.repository.ProfileVideoRepository; import com.crapi.repository.UserDetailsRepository; import com.crapi.repository.UserRepository; @@ -78,6 +81,7 @@ public class UserServiceImplTest { @Mock private SMTPMailServer smtpMailServer; @Mock private ProfileVideoRepository profileVideoRepository; @Mock private ChangeEmailRepository changeEmailRepository; + @Mock private ChangePhoneRepository changePhoneRepository; @Mock Appender appender; @Captor ArgumentCaptor logCaptor; @@ -496,6 +500,135 @@ public void testJwtTokenVerifyWithInvalidToken() { Assertions.assertEquals(UserMessage.INVALID_JWT_TOKEN, crapiResponse.getMessage()); } + @Test + public void changePhoneRequestSuccess() { + ChangePhoneForm changePhoneForm = getDummyChangePhoneForm(); + User user = getDummyUser(); + String expectedMessage = UserMessage.CHANGE_PHONE_MESSAGE + changePhoneForm.getNew_number(); + ChangePhoneRequest changePhoneRequest = getDummyChangePhoneRequest(); + Mockito.when(userRepository.existsByNumber(changePhoneForm.getNew_number())).thenReturn(false); + Mockito.when(userRepository.existsByNumber(changePhoneForm.getOld_number())).thenReturn(true); + Mockito.doReturn(user).when(userService).getUserFromToken(Mockito.any()); + Mockito.doReturn(changePhoneRequest).when(changePhoneRepository).save(Mockito.any()); + Mockito.when(changePhoneRepository.findByUser(user)).thenReturn(changePhoneRequest); + Mockito.doNothing() + .when(smtpMailServer) + .sendMail(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); + + CRAPIResponse crapiResponse = + userService.changePhoneRequest(getMockHttpRequest(), changePhoneForm); + Mockito.verify(smtpMailServer, Mockito.times(1)) + .sendMail(Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); + Assertions.assertEquals(expectedMessage, crapiResponse.getMessage()); + Assertions.assertEquals(HttpStatus.OK.value(), crapiResponse.getStatus()); + } + + @Test + public void changePhoneRequestOldPhoneDoesNotExists() { + ChangePhoneForm changePhoneForm = getDummyChangePhoneForm(); + String expectedMessage = UserMessage.NUMBER_NOT_REGISTERED + changePhoneForm.getOld_number(); + Mockito.when(userRepository.existsByNumber(changePhoneForm.getOld_number())).thenReturn(false); + CRAPIResponse crapiResponse = + userService.changePhoneRequest(getMockHttpRequest(), changePhoneForm); + Assertions.assertEquals(expectedMessage, crapiResponse.getMessage()); + Assertions.assertEquals(HttpStatus.NOT_FOUND.value(), crapiResponse.getStatus()); + } + + @Test + public void changePhoneRequestNewPhoneAlreadyExists() { + ChangePhoneForm changePhoneForm = getDummyChangePhoneForm(); + String expectedMessage = + UserMessage.NUMBER_ALREADY_REGISTERED + changePhoneForm.getNew_number(); + Mockito.when(userRepository.existsByNumber(changePhoneForm.getNew_number())).thenReturn(true); + CRAPIResponse crapiResponse = + userService.changePhoneRequest(getMockHttpRequest(), changePhoneForm); + Assertions.assertEquals(expectedMessage, crapiResponse.getMessage()); + Assertions.assertEquals(HttpStatus.FORBIDDEN.value(), crapiResponse.getStatus()); + } + + @Test + public void verifyPhoneOTPSuccessful() { + ChangePhoneRequest changePhoneRequest = getDummyChangePhoneRequest(); + User user = getDummyUser(); + user.setNumber(changePhoneRequest.getOldPhone()); + String expectedMessage = UserMessage.NUMBER_CHANGE_SUCCESSFUL; + ChangePhoneForm changePhoneForm = getDummyChangePhoneForm(); + Mockito.when(changePhoneRepository.findByUser(Mockito.any())).thenReturn(changePhoneRequest); + Mockito.doReturn(user).when(userService).getUserFromToken(Mockito.any()); + CRAPIResponse crapiResponse = userService.verifyPhoneOTP(getMockHttpRequest(), changePhoneForm); + Assertions.assertEquals(expectedMessage, crapiResponse.getMessage()); + Assertions.assertEquals(HttpStatus.OK.value(), crapiResponse.getStatus()); + } + + @Test + public void verifyOTPFailWhenChangePhoneRequestIsNull() { + User user = getDummyUser(); + String expectedMessage = UserMessage.INVALID_CREDENTIALS; + ChangePhoneForm changePhoneForm = getDummyChangePhoneForm(); + Mockito.doReturn(user).when(userService).getUserFromToken(Mockito.any()); + Mockito.when(changePhoneRepository.findByUser(user)).thenReturn(null); + CRAPIResponse crapiResponse = userService.verifyPhoneOTP(getMockHttpRequest(), changePhoneForm); + Assertions.assertEquals(expectedMessage, crapiResponse.getMessage()); + Assertions.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), crapiResponse.getStatus()); + } + + @Test + public void verifyOTPFailWhenOTPIsNull() { + ChangePhoneRequest changePhoneRequest = getDummyChangePhoneRequest(); + User user = getDummyUser(); + String expectedMessage = UserMessage.INVALID_OTP; + ChangePhoneForm changePhoneForm = getDummyChangePhoneForm(); + changePhoneForm.setOtp(null); + Mockito.doReturn(user).when(userService).getUserFromToken(Mockito.any()); + Mockito.when(changePhoneRepository.findByUser(user)).thenReturn(changePhoneRequest); + CRAPIResponse crapiResponse = userService.verifyPhoneOTP(getMockHttpRequest(), changePhoneForm); + Assertions.assertEquals(expectedMessage, crapiResponse.getMessage()); + Assertions.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), crapiResponse.getStatus()); + } + + @Test + public void verifyOTPFailWhenOTPNotMatch() { + ChangePhoneRequest changePhoneRequest = getDummyChangePhoneRequest(); + User user = getDummyUser(); + String expectedMessage = UserMessage.INVALID_OTP; + ChangePhoneForm changePhoneForm = getDummyChangePhoneForm(); + changePhoneForm.setOtp("4321"); + Mockito.doReturn(user).when(userService).getUserFromToken(Mockito.any()); + Mockito.when(changePhoneRepository.findByUser(user)).thenReturn(changePhoneRequest); + CRAPIResponse crapiResponse = userService.verifyPhoneOTP(getMockHttpRequest(), changePhoneForm); + Assertions.assertEquals(expectedMessage, crapiResponse.getMessage()); + Assertions.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), crapiResponse.getStatus()); + } + + @Test + public void verifyOTPFailWhenOldNumberNotMatch() { + ChangePhoneRequest changePhoneRequest = getDummyChangePhoneRequest(); + User user = getDummyUser(); + String expectedMessage = UserMessage.OLD_NUMBER_DOES_NOT_BELONG; + ChangePhoneForm changePhoneForm = getDummyChangePhoneForm(); + changePhoneForm.setOld_number("1"); + Mockito.doReturn(user).when(userService).getUserFromToken(Mockito.any()); + Mockito.when(changePhoneRepository.findByUser(user)).thenReturn(changePhoneRequest); + CRAPIResponse crapiResponse = userService.verifyPhoneOTP(getMockHttpRequest(), changePhoneForm); + Assertions.assertEquals(expectedMessage, crapiResponse.getMessage()); + Assertions.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), crapiResponse.getStatus()); + } + + @Test + public void verifyOTPFailWhenNewNumberNotMatch() { + ChangePhoneRequest changePhoneRequest = getDummyChangePhoneRequest(); + User user = getDummyUser(); + user.setNumber(changePhoneRequest.getOldPhone()); + String expectedMessage = UserMessage.NEW_NUMBER_DOES_NOT_BELONG; + ChangePhoneForm changePhoneForm = getDummyChangePhoneForm(); + changePhoneForm.setNew_number("1"); + Mockito.doReturn(user).when(userService).getUserFromToken(Mockito.any()); + Mockito.when(changePhoneRepository.findByUser(user)).thenReturn(changePhoneRequest); + CRAPIResponse crapiResponse = userService.verifyPhoneOTP(getMockHttpRequest(), changePhoneForm); + Assertions.assertEquals(expectedMessage, crapiResponse.getMessage()); + Assertions.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), crapiResponse.getStatus()); + } + private LoginWithEmailToken getDummyLoginWithEmailToken() { LoginWithEmailToken loginWithEmailToken = new LoginWithEmailToken(); loginWithEmailToken.setEmail("user@email.com"); @@ -580,4 +713,23 @@ private LoginForm getDummyLoginFormWithoutPassword() { private MockHttpServletRequest getMockHttpRequest() { return new MockHttpServletRequest(); } + + private ChangePhoneForm getDummyChangePhoneForm() { + ChangePhoneForm changePhoneForm = new ChangePhoneForm(); + changePhoneForm.setOtp("1234"); + changePhoneForm.setNew_number("12345679"); + changePhoneForm.setOld_number("12345678"); + return changePhoneForm; + } + + private ChangePhoneRequest getDummyChangePhoneRequest() { + ChangePhoneRequest changePhoneRequest = new ChangePhoneRequest(); + changePhoneRequest.setOldPhone("12345678"); + changePhoneRequest.setNewPhone("12345679"); + changePhoneRequest.setOtp("1234"); + changePhoneRequest.setStatus("DUMMY"); + changePhoneRequest.setUser(getDummyUser()); + changePhoneRequest.setId(1l); + return changePhoneRequest; + } } diff --git a/services/web/package.json b/services/web/package.json index 993cae8f..30509249 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -40,8 +40,8 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "lint": "prettier --check src/**/*.{js,jsx}", - "lint:fix": "prettier --write src/**/*.{js,jsx}" + "lint": "prettier --check src/**/*.{js,jsx,ts,tsx}", + "lint:fix": "prettier --write src/**/*.{js,jsx,ts,tsx}" }, "eslintConfig": { "extends": [ diff --git a/services/web/src/actions/communityActions.ts b/services/web/src/actions/communityActions.ts index 5e7839ac..b829478f 100644 --- a/services/web/src/actions/communityActions.ts +++ b/services/web/src/actions/communityActions.ts @@ -29,21 +29,33 @@ interface AddCommentPayload extends PostByIdPayload { comment: string; } -export const getPostsAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const getPostsAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.GET_POSTS, payload: { accessToken, ...data, callback }, }; }; -export const addPostAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const addPostAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.ADD_POST, payload: { accessToken, ...data, callback }, }; }; -export const getPostByIdAction = ({ accessToken, callback, postId }: PostByIdPayload) => { +export const getPostByIdAction = ({ + accessToken, + callback, + postId, +}: PostByIdPayload) => { return { type: actionTypes.GET_POST_BY_ID, payload: { accessToken, postId, callback }, diff --git a/services/web/src/actions/profileActions.ts b/services/web/src/actions/profileActions.ts index 5af9dfd7..b231e5b8 100644 --- a/services/web/src/actions/profileActions.ts +++ b/services/web/src/actions/profileActions.ts @@ -21,7 +21,11 @@ interface ActionPayload { [key: string]: any; } -export const uploadProfilePicAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const uploadProfilePicAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.UPLOAD_PROFILE_PIC, payload: { @@ -32,7 +36,11 @@ export const uploadProfilePicAction = ({ accessToken, callback, ...data }: Actio }; }; -export const uploadVideoAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const uploadVideoAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.UPLOAD_VIDEO, payload: { @@ -43,7 +51,11 @@ export const uploadVideoAction = ({ accessToken, callback, ...data }: ActionPayl }; }; -export const changeVideoNameAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const changeVideoNameAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.CHANGE_VIDEO_NAME, payload: { @@ -60,7 +72,11 @@ interface ConvertVideoPayload { callback: () => void; } -export const convertVideoAction = ({ accessToken, videoId, callback }: ConvertVideoPayload) => { +export const convertVideoAction = ({ + accessToken, + videoId, + callback, +}: ConvertVideoPayload) => { return { type: actionTypes.CONVERT_VIDEO, payload: { diff --git a/services/web/src/actions/shopActions.ts b/services/web/src/actions/shopActions.ts index 270eab5a..b4d39de3 100644 --- a/services/web/src/actions/shopActions.ts +++ b/services/web/src/actions/shopActions.ts @@ -25,7 +25,11 @@ interface OrderByIdPayload extends ActionPayload { orderId: string; } -export const getProductsAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const getProductsAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.GET_PRODUCTS, payload: { @@ -36,7 +40,11 @@ export const getProductsAction = ({ accessToken, callback, ...data }: ActionPayl }; }; -export const buyProductAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const buyProductAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.BUY_PRODUCT, payload: { @@ -47,7 +55,11 @@ export const buyProductAction = ({ accessToken, callback, ...data }: ActionPaylo }; }; -export const getOrdersAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const getOrdersAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.GET_ORDERS, payload: { @@ -58,7 +70,11 @@ export const getOrdersAction = ({ accessToken, callback, ...data }: ActionPayloa }; }; -export const getOrderByIdAction = ({ accessToken, orderId, callback }: OrderByIdPayload) => { +export const getOrderByIdAction = ({ + accessToken, + orderId, + callback, +}: OrderByIdPayload) => { return { type: actionTypes.GET_ORDER_BY_ID, payload: { @@ -69,7 +85,11 @@ export const getOrderByIdAction = ({ accessToken, orderId, callback }: OrderById }; }; -export const returnOrderAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const returnOrderAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.RETURN_ORDER, payload: { @@ -80,7 +100,11 @@ export const returnOrderAction = ({ accessToken, callback, ...data }: ActionPayl }; }; -export const applyCouponAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const applyCouponAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.APPLY_COUPON, payload: { diff --git a/services/web/src/actions/userActions.ts b/services/web/src/actions/userActions.ts index 94c413fb..82a0cf90 100644 --- a/services/web/src/actions/userActions.ts +++ b/services/web/src/actions/userActions.ts @@ -52,13 +52,23 @@ interface VerifyOTPPayload extends ActionPayload { password: string; } +interface VerifyPhoneChangeOTPPayload extends ActionPayload { + otp: string; + old_number: string; + new_number: string; +} + interface ResetPasswordPayload extends ActionPayload { email: string; accessToken: string; password: string; } -export const logInUserAction = ({ email, password, callback }: LoginPayload) => { +export const logInUserAction = ({ + email, + password, + callback, +}: LoginPayload) => { return { type: actionTypes.LOG_IN, payload: { email, password, callback }, @@ -73,7 +83,11 @@ export const unlockUserAction = ({ email, code, callback }: UnlockPayload) => { }; }; -export const unlockRedirectUserAction = ({ email, message, callback }: UnlockRedirectPayload) => { +export const unlockRedirectUserAction = ({ + email, + message, + callback, +}: UnlockRedirectPayload) => { console.log("unlockRedirectUserAction", email, message, callback); return { type: actionTypes.UNLOCK_USER_REDIRECT, @@ -102,7 +116,9 @@ export const logOutUserAction = ({ callback }: ActionPayload) => { }; }; -export const validateAccessTokenAction = ({ accessToken }: AccessTokenPayload) => { +export const validateAccessTokenAction = ({ + accessToken, +}: AccessTokenPayload) => { console.log("validateAccessTokenAction action"); return { type: actionTypes.VALIDATE_ACCESS_TOKEN, @@ -123,13 +139,31 @@ export const forgotPasswordAction = ({ email, callback }: LoginPayload) => { }; }; -export const verifyOTPAction = ({ otp, email, password, callback }: VerifyOTPPayload) => { +export const verifyOTPAction = ({ + otp, + email, + password, + callback, +}: VerifyOTPPayload) => { return { type: actionTypes.VERIFY_OTP, payload: { otp, email, password, callback }, }; }; +export const verifyPhoneChangeOTPAction = ({ + accessToken, + otp, + old_number, + new_number, + callback, +}: VerifyPhoneChangeOTPPayload) => { + return { + type: actionTypes.VERIFY_PHONE_NUMBER_OTP, + payload: { accessToken, otp, old_number, new_number, callback }, + }; +}; + export const resetPasswordAction = ({ email, accessToken, @@ -142,35 +176,68 @@ export const resetPasswordAction = ({ }; }; -export const getMechanicServicesAction = ({ accessToken, callback, ...data }: ActionPayload & AccessTokenPayload) => { +export const getMechanicServicesAction = ({ + accessToken, + callback, + ...data +}: ActionPayload & AccessTokenPayload) => { return { type: actionTypes.GET_MECHANIC_SERVICES, payload: { accessToken, callback, ...data }, }; }; -export const getVehicleServicesAction = ({ accessToken, VIN, callback, ...data }: ActionPayload & AccessTokenPayload) => { +export const getVehicleServicesAction = ({ + accessToken, + VIN, + callback, + ...data +}: ActionPayload & AccessTokenPayload) => { return { type: actionTypes.GET_VEHICLE_SERVICES, payload: { accessToken, VIN, callback, ...data }, }; }; -export const getServiceReportAction = ({ accessToken, reportId, callback, ...data }: ActionPayload & AccessTokenPayload) => { +export const getServiceReportAction = ({ + accessToken, + reportId, + callback, + ...data +}: ActionPayload & AccessTokenPayload) => { return { type: actionTypes.GET_SERVICE_REPORT, payload: { accessToken, reportId, callback, ...data }, }; }; -export const changeEmailAction = ({ accessToken, callback, ...data }: ActionPayload & AccessTokenPayload) => { +export const changeEmailAction = ({ + accessToken, + callback, + ...data +}: ActionPayload & AccessTokenPayload) => { return { type: actionTypes.CHANGE_EMAIL, payload: { accessToken, callback, ...data }, }; }; -export const verifyTokenAction = ({ accessToken, callback, ...data }: ActionPayload & AccessTokenPayload) => { +export const changePhoneNumberAction = ({ + accessToken, + callback, + ...data +}: ActionPayload & AccessTokenPayload) => { + return { + type: actionTypes.CHANGE_PHONE_NUMBER, + payload: { accessToken, callback, ...data }, + }; +}; + +export const verifyTokenAction = ({ + accessToken, + callback, + ...data +}: ActionPayload & AccessTokenPayload) => { return { type: actionTypes.VERIFY_TOKEN, payload: { accessToken, callback, ...data }, diff --git a/services/web/src/actions/vehicleActions.ts b/services/web/src/actions/vehicleActions.ts index 43e616a2..371408b5 100644 --- a/services/web/src/actions/vehicleActions.ts +++ b/services/web/src/actions/vehicleActions.ts @@ -25,7 +25,11 @@ interface GetVehiclesPayload extends ActionPayload { email?: string; } -export const verifyVehicleAction = ({ callback, accessToken, ...data }: ActionPayload) => { +export const verifyVehicleAction = ({ + callback, + accessToken, + ...data +}: ActionPayload) => { return { type: actionTypes.VERIFY_VEHICLE, payload: { @@ -36,7 +40,11 @@ export const verifyVehicleAction = ({ callback, accessToken, ...data }: ActionPa }; }; -export const getMechanicsAction = ({ callback, accessToken, ...data }: ActionPayload) => { +export const getMechanicsAction = ({ + callback, + accessToken, + ...data +}: ActionPayload) => { return { type: actionTypes.GET_MECHANICS, payload: { @@ -73,7 +81,11 @@ export const resendMailAction = ({ callback, accessToken }: ActionPayload) => { }; }; -export const contactMechanicAction = ({ callback, accessToken, ...data }: ActionPayload) => { +export const contactMechanicAction = ({ + callback, + accessToken, + ...data +}: ActionPayload) => { return { type: actionTypes.CONTACT_MECHANIC, payload: { @@ -84,7 +96,11 @@ export const contactMechanicAction = ({ callback, accessToken, ...data }: Action }; }; -export const refreshLocationAction = ({ accessToken, callback, ...data }: ActionPayload) => { +export const refreshLocationAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { return { type: actionTypes.REFRESH_LOCATION, payload: { diff --git a/services/web/src/components/changePhoneNumber/changePhoneNumber.tsx b/services/web/src/components/changePhoneNumber/changePhoneNumber.tsx new file mode 100644 index 00000000..187f69de --- /dev/null +++ b/services/web/src/components/changePhoneNumber/changePhoneNumber.tsx @@ -0,0 +1,74 @@ +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Card, Steps } from "antd"; + +import React, { useState } from "react"; + +import PropTypes from "prop-types"; + +import newPhoneNumberFormContainer from "../../containers/newPhoneNumberForm/newPhoneNumberForm"; +import otpPhoneChangeFormContainer from "../../containers/otpPhoneChangeForm/otpPhoneChangeForm"; + +const { Step } = Steps; + +interface StepType { + title: string; + component: React.ComponentType; +} + +const ChangePhoneNumber: React.FC = () => { + const steps: StepType[] = [ + { + title: "New Phone Number", + component: newPhoneNumberFormContainer, + }, + { + title: "OTP Verification", + component: otpPhoneChangeFormContainer, + }, + ]; + + const [number, setNumber] = useState(""); + const [currentStep, setCurrentStep] = useState(0); + + const handleStepChange = (step: number) => setCurrentStep(step); + + const handlePhoneNumberChange = (newNumber: string) => setNumber(newNumber); + + const CurrentComponent = steps[currentStep].component; + + return ( +
+ + + {steps.map((step) => ( + + ))} + +
+ +
+
+
+ ); +}; + +export default ChangePhoneNumber; diff --git a/services/web/src/components/dashboard/dashboard.tsx b/services/web/src/components/dashboard/dashboard.tsx index c2dd043b..4aff0641 100644 --- a/services/web/src/components/dashboard/dashboard.tsx +++ b/services/web/src/components/dashboard/dashboard.tsx @@ -65,7 +65,11 @@ interface RootState { }; } -const vehicleCardHeader = (vehicle: Vehicle, handleVehicleServiceClick: (vin: string) => void, handleContactMechanic: (vin: string) => void) => { +const vehicleCardHeader = ( + vehicle: Vehicle, + handleVehicleServiceClick: (vin: string) => void, + handleContactMechanic: (vin: string) => void, +) => { return ( ({ - vehicles: state.vehicleReducer.vehicles, - }) -); +const connector = connect((state: RootState) => ({ + vehicles: state.vehicleReducer.vehicles, +})); type PropsFromRedux = ConnectedProps; @@ -109,7 +111,11 @@ interface DashboardProps extends PropsFromRedux { resendMail: () => void; } -const Dashboard: React.FC = ({ vehicles, refreshLocation, resendMail }) => { +const Dashboard: React.FC = ({ + vehicles, + refreshLocation, + resendMail, +}) => { const navigate = useNavigate(); const vehicleCardContent = (vehicle: Vehicle) => ( <> @@ -217,7 +223,11 @@ const Dashboard: React.FC = ({ vehicles, refreshLocation, resend diff --git a/services/web/src/components/emailForm/emailForm.tsx b/services/web/src/components/emailForm/emailForm.tsx index 1b513c83..3a559904 100644 --- a/services/web/src/components/emailForm/emailForm.tsx +++ b/services/web/src/components/emailForm/emailForm.tsx @@ -55,7 +55,9 @@ const EmailForm: React.FC = ({ ) => onMailChange(event.target.value)} + onChange={(event: React.ChangeEvent) => + onMailChange(event.target.value) + } /> diff --git a/services/web/src/components/layout/layout.tsx b/services/web/src/components/layout/layout.tsx index afc7c62f..f807c7db 100644 --- a/services/web/src/components/layout/layout.tsx +++ b/services/web/src/components/layout/layout.tsx @@ -46,6 +46,7 @@ import { validateAccessTokenAction, } from "../../actions/userActions"; import { isAccessTokenValid } from "../../utils"; +import ChangePhoneNumber from "../changePhoneNumber/changePhoneNumber"; const { Content } = Layout; @@ -276,6 +277,18 @@ const StyledComp: React.FC = (props) => { /> } /> + + } + /> { + const { onFinish, onPhoneNumberChange, hasErrored, errorMessage } = props; + return ( +
+ + onPhoneNumberChange(event.target.value)} + /> + + + {hasErrored &&
{errorMessage}
} + +
+
+ ); +}; + +NewPhoneNumberForm.propTypes = { + onFinish: PropTypes.func, + hasErrored: PropTypes.bool, + errorMessage: PropTypes.string, + onPhoneNumberChange: PropTypes.func, +}; + +export default NewPhoneNumberForm; diff --git a/services/web/src/components/newPost/newPost.tsx b/services/web/src/components/newPost/newPost.tsx index 369c7f8e..89ce466c 100644 --- a/services/web/src/components/newPost/newPost.tsx +++ b/services/web/src/components/newPost/newPost.tsx @@ -26,7 +26,11 @@ interface NewPostProps { onFinish: (values: any) => void; } -const NewPost: React.FC = ({ hasErrored, errorMessage, onFinish }) => { +const NewPost: React.FC = ({ + hasErrored, + errorMessage, + onFinish, +}) => { const urlParams = new URLSearchParams(window.location.search); const postContent = urlParams.get("content"); diff --git a/services/web/src/components/otpChangePhoneForm/otpChangePhoneForm.tsx b/services/web/src/components/otpChangePhoneForm/otpChangePhoneForm.tsx new file mode 100644 index 00000000..c80f4c49 --- /dev/null +++ b/services/web/src/components/otpChangePhoneForm/otpChangePhoneForm.tsx @@ -0,0 +1,60 @@ +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react"; +import { Button, Form, Input } from "antd"; +import { OTP_REQUIRED } from "../../constants/messages"; + +interface OTPChangePhoneFormProps { + onFinish: (values: any) => void; + errorMessage: string; + hasErrored: boolean; +} + +const OTPChangePhoneForm: React.FC = ({ + onFinish, + errorMessage, + hasErrored, +}) => { + return ( +
+ + + + + {hasErrored &&
{errorMessage}
} + +
+
+ ); +}; + +export default OTPChangePhoneForm; diff --git a/services/web/src/components/otpForm/otpForm.tsx b/services/web/src/components/otpForm/otpForm.tsx index 686f4ee4..04859a11 100644 --- a/services/web/src/components/otpForm/otpForm.tsx +++ b/services/web/src/components/otpForm/otpForm.tsx @@ -30,7 +30,11 @@ interface OtpFormProps { hasErrored: boolean; } -const OtpForm: React.FC = ({ onFinish, errorMessage, hasErrored }) => { +const OtpForm: React.FC = ({ + onFinish, + errorMessage, + hasErrored, +}) => { return (
= (props) => { const videoInputRef = useRef(null); const takeVideoAction = (input: { key: string }) => { - if (input.key === "1" && videoInputRef.current) videoInputRef.current.click(); + if (input.key === "1" && videoInputRef.current) + videoInputRef.current.click(); if (input.key === "2") setIsVideoModalOpen(true); if (input.key === "3") shareVideoWithCommunity(); }; @@ -156,6 +157,15 @@ const Profile: React.FC = (props) => { {userData.number} + @@ -238,7 +248,13 @@ const Profile: React.FC = (props) => { ); }; -const mapStateToProps = ({ userReducer, profileReducer }: { userReducer: UserData; profileReducer: ProfileData }) => { +const mapStateToProps = ({ + userReducer, + profileReducer, +}: { + userReducer: UserData; + profileReducer: ProfileData; +}) => { return { userData: userReducer, profileData: profileReducer }; }; diff --git a/services/web/src/components/resetPassword/resetPassword.tsx b/services/web/src/components/resetPassword/resetPassword.tsx index 5b364930..623119af 100644 --- a/services/web/src/components/resetPassword/resetPassword.tsx +++ b/services/web/src/components/resetPassword/resetPassword.tsx @@ -30,7 +30,11 @@ interface ResetPasswordProps { onFinish: (values: any) => void; } -const ResetPassword: React.FC = ({ hasErrored, errorMessage, onFinish }) => { +const ResetPassword: React.FC = ({ + hasErrored, + errorMessage, + onFinish, +}) => { return (
diff --git a/services/web/src/components/serviceReport/serviceReport.tsx b/services/web/src/components/serviceReport/serviceReport.tsx index 04e5b317..476ad889 100644 --- a/services/web/src/components/serviceReport/serviceReport.tsx +++ b/services/web/src/components/serviceReport/serviceReport.tsx @@ -31,7 +31,7 @@ interface Vehicle { interface Mechanic { mechanic_code: string; - user: Owner + user: Owner; } interface Service { @@ -52,57 +52,88 @@ const ServiceReport: React.FC = ({ service }) => { console.log("Service is undefined"); return ( - + ); } - + return ( - - + + - {service.id} - {service.status} - {service.created_on} - {service.problem_details} + + {service.id} + + + {service.status} + + + {service.created_on} + + + {service.problem_details} + - + - {service.mechanic.mechanic_code} - {service.mechanic.user.email} + + {service.mechanic.mechanic_code} + + + {service.mechanic.user.email} + - + - {service.vehicle.vin} + + {service.vehicle.vin} + - + - {service.vehicle.owner.email} - {service.vehicle.owner.number} + + {service.vehicle.owner.email} + + + {service.vehicle.owner.number} + diff --git a/services/web/src/components/shop/shop.tsx b/services/web/src/components/shop/shop.tsx index cabdc686..ed582d33 100644 --- a/services/web/src/components/shop/shop.tsx +++ b/services/web/src/components/shop/shop.tsx @@ -144,9 +144,17 @@ const Shop: React.FC = (props) => { {products.map((product) => ( - }> + } + > } + description={ + + } /> @@ -217,7 +225,8 @@ interface RootState { } const mapStateToProps = (state: RootState) => { - const { accessToken, availableCredit, products, prevOffset, nextOffset } = state.shopReducer; + const { accessToken, availableCredit, products, prevOffset, nextOffset } = + state.shopReducer; return { accessToken, availableCredit, products, prevOffset, nextOffset }; }; diff --git a/services/web/src/components/tokenForm/tokenForm.tsx b/services/web/src/components/tokenForm/tokenForm.tsx index a923994b..22f457e9 100644 --- a/services/web/src/components/tokenForm/tokenForm.tsx +++ b/services/web/src/components/tokenForm/tokenForm.tsx @@ -23,7 +23,11 @@ interface TokenFormProps { hasErrored?: boolean; } -const TokenForm: React.FC = ({ onFinish, errorMessage, hasErrored }) => { +const TokenForm: React.FC = ({ + onFinish, + errorMessage, + hasErrored, +}) => { return ( = ({ services }) => { +const VehicleServiceDashboard: React.FC = ({ + services, +}) => { const urlParams = new URLSearchParams(window.location.search); const VIN = urlParams.get("VIN"); return ( @@ -51,14 +53,13 @@ const VehicleServiceDashboard: React.FC = ({ servi {services.map((service: Service) => ( - - -

- Vehicle VIN: {service.vehicle.vin} -

+ + +

Vehicle VIN: {service.vehicle.vin}

Owner email-id: {service.vehicle.owner.email} diff --git a/services/web/src/components/verifyVehicle/verifyVehicle.tsx b/services/web/src/components/verifyVehicle/verifyVehicle.tsx index b803e605..ccc03616 100644 --- a/services/web/src/components/verifyVehicle/verifyVehicle.tsx +++ b/services/web/src/components/verifyVehicle/verifyVehicle.tsx @@ -29,7 +29,11 @@ interface VerifyVehicleProps { onFinish: (values: any) => void; } -const VerifyVehicle: React.FC = ({ hasErrored, errorMessage, onFinish }) => { +const VerifyVehicle: React.FC = ({ + hasErrored, + errorMessage, + onFinish, +}) => { return (

()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -export const PHONE_VALIDATION: RegExp = /^(\+\d{1,2}\s?)?1?-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/; +export const PHONE_VALIDATION: RegExp = + /^(\+\d{1,2}\s?)?1?-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/; export const PASSWORD_VALIDATION: RegExp = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$@!%&*?])[A-Za-z\d#$@!%&*?]{8,16}$/; export const NAME_VALIDATION: RegExp = /^[a-zA-Z ]+$/; diff --git a/services/web/src/containers/mechanicDashboard/mechanicDashboard.tsx b/services/web/src/containers/mechanicDashboard/mechanicDashboard.tsx index b31bf5c2..5a09a2b3 100644 --- a/services/web/src/containers/mechanicDashboard/mechanicDashboard.tsx +++ b/services/web/src/containers/mechanicDashboard/mechanicDashboard.tsx @@ -52,7 +52,10 @@ const connector = connect(mapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; -const MechanicDashboardContainer: React.FC = ({ accessToken, getServices }) => { +const MechanicDashboardContainer: React.FC = ({ + accessToken, + getServices, +}) => { const [services, setServices] = useState([]); useEffect(() => { diff --git a/services/web/src/containers/newPhoneNumberForm/newPhoneNumberForm.js b/services/web/src/containers/newPhoneNumberForm/newPhoneNumberForm.js new file mode 100644 index 00000000..2794ad3e --- /dev/null +++ b/services/web/src/containers/newPhoneNumberForm/newPhoneNumberForm.js @@ -0,0 +1,83 @@ +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Modal } from "antd"; +import React from "react"; + +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import responseTypes from "../../constants/responseTypes"; +import { SUCCESS_MESSAGE } from "../../constants/messages"; +import NewPhoneNumberForm from "../../components/newPhoneNumberForm/newPhoneNumberForm"; +import { changePhoneNumberAction } from "../../actions/userActions"; + +const NewPhoneNumberFormContainer = (props) => { + const { accessToken, oldPhoneNumber, onPhoneNumberChange } = props; + + const [hasErrored, setHasErrored] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(""); + + const callback = (res, data) => { + if (res === responseTypes.SUCCESS) { + Modal.success({ + title: SUCCESS_MESSAGE, + content: data, + onOk: () => props.setCurrentStep(props.currentStep + 1), + }); + } else { + setHasErrored(true); + setErrorMessage(data); + } + }; + + const onFinish = (values) => { + props.changePhoneNumber({ + ...values, + callback, + accessToken, + old_number: oldPhoneNumber, + }); + }; + + return ( + + ); +}; + +const mapStateToProps = ({ userReducer: { accessToken, number } }) => { + return { accessToken, oldPhoneNumber: number }; +}; + +const mapDispatchToProps = { + changePhoneNumber: changePhoneNumberAction, +}; + +NewPhoneNumberFormContainer.propTypes = { + oldPhoneNumber: PropTypes.string, + accessToken: PropTypes.string, + changePhoneNumber: PropTypes.func, + currentStep: PropTypes.number, + setCurrentStep: PropTypes.func, + onPhoneNumberChange: PropTypes.func, +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(NewPhoneNumberFormContainer); diff --git a/services/web/src/containers/otpPhoneChangeForm/otpPhoneChangeForm.js b/services/web/src/containers/otpPhoneChangeForm/otpPhoneChangeForm.js new file mode 100644 index 00000000..a1522e91 --- /dev/null +++ b/services/web/src/containers/otpPhoneChangeForm/otpPhoneChangeForm.js @@ -0,0 +1,83 @@ +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { Modal } from "antd"; +import { verifyPhoneChangeOTPAction } from "../../actions/userActions"; +import responseTypes from "../../constants/responseTypes"; +import { SUCCESS_MESSAGE } from "../../constants/messages"; +import { useNavigate } from "react-router-dom"; +import OTPChangePhoneForm from "../../components/otpChangePhoneForm/otpChangePhoneForm"; + +const OtpPhoneChangeFormContainer = (props) => { + const { accessToken, oldPhoneNumber } = props; + const navigate = useNavigate(); + const [hasErrored, setHasErrored] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(""); + + const callback = (res, data) => { + if (res === responseTypes.SUCCESS) { + Modal.success({ + title: SUCCESS_MESSAGE, + content: data, + onOk: () => navigate("/my-profile"), + }); + } else { + setHasErrored(true); + setErrorMessage(data); + } + }; + + const onFinish = (values) => { + const { accessToken, number, newPhoneNumber } = props; + props.verifyPhoneChangeOTP({ + ...values, + callback, + accessToken, + new_number: number, + old_number: oldPhoneNumber, + }); + }; + + return ( + + ); +}; + +const mapDispatchToProps = { + verifyPhoneChangeOTP: verifyPhoneChangeOTPAction, +}; + +const mapStateToProps = ({ userReducer: { accessToken, number } }) => { + return { accessToken, oldPhoneNumber: number }; +}; + +OtpPhoneChangeFormContainer.propTypes = { + verifyPhoneChangeOTP: PropTypes.func, + oldPhoneNumber: PropTypes.string, + newPhoneNumber: PropTypes.string, + accessToken: PropTypes.string, +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(OtpPhoneChangeFormContainer); diff --git a/services/web/src/containers/serviceReport/serviceReport.tsx b/services/web/src/containers/serviceReport/serviceReport.tsx index 03656ff0..de7ddc27 100644 --- a/services/web/src/containers/serviceReport/serviceReport.tsx +++ b/services/web/src/containers/serviceReport/serviceReport.tsx @@ -62,7 +62,10 @@ const connector = connect(mapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; -const ServiceReportContainer: React.FC = ({ accessToken, getServiceReport }) => { +const ServiceReportContainer: React.FC = ({ + accessToken, + getServiceReport, +}) => { const [service, setService] = useState(); const urlParams = new URLSearchParams(window.location.search); const reportId = urlParams.get("id"); @@ -80,7 +83,7 @@ const ServiceReportContainer: React.FC = ({ accessToken, getServ }); } }; - getServiceReport({ accessToken, reportId, callback}); + getServiceReport({ accessToken, reportId, callback }); }, [accessToken, getServiceReport, reportId]); // Ensure that the Service type in the component matches the one from the API diff --git a/services/web/src/containers/vehicleServiceDashboard/vehicleServiceDashboard.tsx b/services/web/src/containers/vehicleServiceDashboard/vehicleServiceDashboard.tsx index 7cee18d6..f03c91d6 100644 --- a/services/web/src/containers/vehicleServiceDashboard/vehicleServiceDashboard.tsx +++ b/services/web/src/containers/vehicleServiceDashboard/vehicleServiceDashboard.tsx @@ -55,7 +55,10 @@ const connector = connect(mapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; -const VehicleServiceDashboardContainer: React.FC = ({ accessToken, getVehicleServiceHistory }) => { +const VehicleServiceDashboardContainer: React.FC = ({ + accessToken, + getVehicleServiceHistory, +}) => { const [services, setServices] = useState([]); const urlParams = new URLSearchParams(window.location.search); const VIN = urlParams.get("VIN"); @@ -73,7 +76,7 @@ const VehicleServiceDashboardContainer: React.FC = ({ accessToke }); } }; - getVehicleServiceHistory({ accessToken, VIN, callback}); + getVehicleServiceHistory({ accessToken, VIN, callback }); }, [accessToken, getVehicleServiceHistory, VIN]); // Ensure that the Service type in the component matches the one from the API diff --git a/services/web/src/reducers/userReducer.ts b/services/web/src/reducers/userReducer.ts index 880765a2..3a56e6b6 100644 --- a/services/web/src/reducers/userReducer.ts +++ b/services/web/src/reducers/userReducer.ts @@ -124,6 +124,11 @@ const userReducer = ( ...state, available_credit: maction.payload.availableCredit, }; + case actionTypes.PHONE_NUMBER_VERIFIED: + return { + ...state, + number: maction.payload.new_number, + }; default: return state; } diff --git a/services/web/src/sagas/userSaga.ts b/services/web/src/sagas/userSaga.ts index 3f23d818..b1629cb9 100644 --- a/services/web/src/sagas/userSaga.ts +++ b/services/web/src/sagas/userSaga.ts @@ -334,6 +334,54 @@ export function* verifyOTP(action: MyAction): Generator { } } +/** + * Verify OTP for change Phone Number + + * @payload {string} payload.old_number - User number + * @payload {string} payload.new_number - User new number + * @payload {string} payload.otp - OTP received through mail + */ +export function* verifyPhoneNumberOTP( + action: MyAction, +): Generator { + const { accessToken, old_number, new_number, otp, callback } = action.payload; + + let receivedResponse: Partial = {}; + try { + yield put({ type: actionTypes.FETCHING_DATA }); + const postUrl = + APIService.IDENTITY_SERVICE + requestURLS.VERIFY_PHONE_NUMBER_OTP; + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }; + + const responseJSON = yield fetch(postUrl, { + headers, + method: "POST", + body: JSON.stringify({ old_number, new_number, otp }), + }).then((response: Response) => { + receivedResponse = response; + if (receivedResponse.ok) return response; + return response.json(); + }); + + yield put({ type: actionTypes.FETCHED_DATA, payload: receivedResponse }); + if (receivedResponse.ok) { + callback(responseTypes.SUCCESS, OTP_VERIFIED); + yield put({ + type: actionTypes.PHONE_NUMBER_VERIFIED, + payload: { new_number }, + }); + } else if (receivedResponse.status === 503) + callback(responseTypes.REDIRECT, responseJSON.message); + else callback(responseTypes.FAILURE, responseJSON.message); + } catch (e) { + yield put({ type: actionTypes.FETCHED_DATA, payload: receivedResponse }); + callback(responseTypes.FAILURE, OTP_NOT_VERIFIED); + } +} + /** * Change user password @@ -412,6 +460,48 @@ export function* changeEmail(action: MyAction): Generator { callback(responseTypes.FAILURE, TOKEN_NOT_SENT); } } +/** + * Request for changing email + + * @payload {string} payload.accessToken - Access token of the user + * @payload {string} payload.old_email - Old Phone Number of the user + * @payload {string} payload.new_email - New Phone Number entered by the user + * @payload {Function} payload.callback - Callback method + */ +export function* changePhoneNumber( + action: MyAction, +): Generator { + const { accessToken, old_number, new_number, callback } = action.payload; + let receivedResponse: Partial = {}; + try { + yield put({ type: actionTypes.FETCHING_DATA }); + const postUrl = + APIService.IDENTITY_SERVICE + requestURLS.CHANGE_PHONE_NUMBER; + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }; + + const responseJSON = yield fetch(postUrl, { + headers, + method: "POST", + body: JSON.stringify({ old_number, new_number }), + }).then((response: Response) => { + receivedResponse = response; + return response.json(); + }); + + yield put({ type: actionTypes.FETCHED_DATA, payload: receivedResponse }); + if (receivedResponse.ok) { + callback(responseTypes.SUCCESS, responseJSON.message); + } else { + callback(responseTypes.FAILURE, responseJSON.message); + } + } catch (e) { + yield put({ type: actionTypes.FETCHED_DATA, payload: receivedResponse }); + callback(responseTypes.FAILURE, TOKEN_NOT_SENT); + } +} /** * Verify token and change email @@ -463,4 +553,6 @@ export function* userActionWatcher() { yield takeLatest(actionTypes.RESET_PASSWORD, resetPassword); yield takeLatest(actionTypes.CHANGE_EMAIL, changeEmail); yield takeLatest(actionTypes.VERIFY_TOKEN, verifyToken); + yield takeLatest(actionTypes.CHANGE_PHONE_NUMBER, changePhoneNumber); + yield takeLatest(actionTypes.VERIFY_PHONE_NUMBER_OTP, verifyPhoneNumberOTP); } diff --git a/services/web/src/sagas/vehicleSaga.ts b/services/web/src/sagas/vehicleSaga.ts index 29a2c472..529a7d78 100644 --- a/services/web/src/sagas/vehicleSaga.ts +++ b/services/web/src/sagas/vehicleSaga.ts @@ -306,12 +306,15 @@ export function* refreshLocation(action: MyAction): Generator { * @payload {string} payload.accessToken - Access token of the user * @payload {Function} payload.callback - Callback method */ -export function* getMechanicServices(action: MyAction): Generator { +export function* getMechanicServices( + action: MyAction, +): Generator { const { accessToken, callback } = action.payload; let receivedResponse: Partial = {}; try { yield put({ type: actionTypes.FETCHING_DATA }); - const getUrl = APIService.WORKSHOP_SERVICE + requestURLS.GET_MECHANIC_SERVICES; + const getUrl = + APIService.WORKSHOP_SERVICE + requestURLS.GET_MECHANIC_SERVICES; const headers = { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, @@ -342,13 +345,17 @@ export function* getMechanicServices(action: MyAction): Generator { +export function* getVehicleServices( + action: MyAction, +): Generator { console.log("Vehicle Services", action.payload); const { accessToken, VIN, callback } = action.payload; let receivedResponse: Partial = {}; try { yield put({ type: actionTypes.FETCHING_DATA }); - const getUrl = APIService.WORKSHOP_SERVICE + requestURLS.GET_VEHICLE_SERVICES.replace("", VIN); + const getUrl = + APIService.WORKSHOP_SERVICE + + requestURLS.GET_VEHICLE_SERVICES.replace("", VIN); console.log("Get URL", getUrl); const headers = { "Content-Type": "application/json", @@ -392,7 +399,11 @@ export function* getServiceReport(action: MyAction): Generator { let receivedResponse: Partial = {}; try { yield put({ type: actionTypes.FETCHING_DATA }); - const getUrl = APIService.WORKSHOP_SERVICE + requestURLS.GET_SERVICE_REPORT + "?report_id=" + reportId; + const getUrl = + APIService.WORKSHOP_SERVICE + + requestURLS.GET_SERVICE_REPORT + + "?report_id=" + + reportId; console.log("Get URL", getUrl); const headers = { "Content-Type": "application/json",