Merge pull request #16 from sBubshait/feature/phone-signin
Feature/phone signin
This commit is contained in:
commit
52ddacd31c
@ -55,7 +55,7 @@ public class SecurityConfig {
|
|||||||
.csrf(csrf -> csrf.disable())
|
.csrf(csrf -> csrf.disable())
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/", "/login", "/swagger-ui/**", "/v3/api-docs/**", "/docs/**", "/docs").permitAll()
|
.requestMatchers("/", "/login", "/checkInvitation", "/register", "/swagger-ui/**", "/v3/api-docs/**", "/docs/**", "/docs").permitAll()
|
||||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,9 +8,15 @@ import online.wesal.wesal.dto.LoginRequest;
|
|||||||
import online.wesal.wesal.dto.LoginResponse;
|
import online.wesal.wesal.dto.LoginResponse;
|
||||||
import online.wesal.wesal.dto.UpdateUserRequest;
|
import online.wesal.wesal.dto.UpdateUserRequest;
|
||||||
import online.wesal.wesal.dto.UsernameSelectionRequest;
|
import online.wesal.wesal.dto.UsernameSelectionRequest;
|
||||||
|
import online.wesal.wesal.dto.CheckInvitationRequest;
|
||||||
|
import online.wesal.wesal.dto.RegisterRequest;
|
||||||
|
import online.wesal.wesal.dto.RegisterResponse;
|
||||||
import online.wesal.wesal.entity.User;
|
import online.wesal.wesal.entity.User;
|
||||||
|
import online.wesal.wesal.entity.InvitationCode;
|
||||||
import online.wesal.wesal.service.AuthService;
|
import online.wesal.wesal.service.AuthService;
|
||||||
import online.wesal.wesal.service.UserService;
|
import online.wesal.wesal.service.UserService;
|
||||||
|
import online.wesal.wesal.service.InvitationCodeService;
|
||||||
|
import online.wesal.wesal.config.JwtUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@ -29,6 +35,12 @@ public class AuthController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InvitationCodeService invitationCodeService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtUtil jwtUtil;
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
@Operation(summary = "Health check", description = "Returns API status")
|
@Operation(summary = "Health check", description = "Returns API status")
|
||||||
public ResponseEntity<Map<String, Integer>> healthCheck() {
|
public ResponseEntity<Map<String, Integer>> healthCheck() {
|
||||||
@ -36,10 +48,10 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
@Operation(summary = "User login", description = "Authenticate user with email or username and return JWT token")
|
@Operation(summary = "User login", description = "Authenticate user with phone number or username and return JWT token")
|
||||||
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest loginRequest) {
|
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest loginRequest) {
|
||||||
try {
|
try {
|
||||||
String token = authService.authenticate(loginRequest.getEmailOrUsername(), loginRequest.getPassword());
|
String token = authService.authenticate(loginRequest.getPhoneNumberOrUsername(), loginRequest.getPassword());
|
||||||
return ResponseEntity.ok(new LoginResponse(token));
|
return ResponseEntity.ok(new LoginResponse(token));
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
@ -84,7 +96,7 @@ public class AuthController {
|
|||||||
// Create user data without password
|
// Create user data without password
|
||||||
Map<String, Object> userData = Map.of(
|
Map<String, Object> userData = Map.of(
|
||||||
"id", user.getId(),
|
"id", user.getId(),
|
||||||
"email", user.getEmail(),
|
"phoneNumber", user.getPhoneNumber(),
|
||||||
"username", user.getUsername() != null ? user.getUsername() : "",
|
"username", user.getUsername() != null ? user.getUsername() : "",
|
||||||
"displayName", user.getDisplayName(),
|
"displayName", user.getDisplayName(),
|
||||||
"avatar", user.getAvatar() != null ? user.getAvatar() : "",
|
"avatar", user.getAvatar() != null ? user.getAvatar() : "",
|
||||||
@ -107,14 +119,105 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/checkInvitation")
|
||||||
|
@Operation(summary = "Check invitation code", description = "Validates if an invitation code is valid and not expired")
|
||||||
|
public ResponseEntity<Map<String, Object>> checkInvitation(@Valid @RequestBody CheckInvitationRequest request) {
|
||||||
|
try {
|
||||||
|
boolean isValid = invitationCodeService.isValidCode(request.getCode());
|
||||||
|
if (isValid) {
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", true,
|
||||||
|
"message", "Invitation code is valid",
|
||||||
|
"data", Map.of("code", request.getCode(), "valid", true)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", false,
|
||||||
|
"message", "Invalid or expired invitation code",
|
||||||
|
"data", Map.of("code", request.getCode(), "valid", false)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", false,
|
||||||
|
"message", "Error validating invitation code"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
@Operation(summary = "Register new user", description = "Register a new user with invitation code, phone number, and password")
|
||||||
|
public ResponseEntity<Map<String, Object>> register(@Valid @RequestBody RegisterRequest request) {
|
||||||
|
try {
|
||||||
|
User user = invitationCodeService.registerUserWithCode(
|
||||||
|
request.getCode(),
|
||||||
|
request.getPhoneNumber(),
|
||||||
|
request.getPassword()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-login: generate JWT token
|
||||||
|
String token = jwtUtil.generateToken(user.getPhoneNumber());
|
||||||
|
|
||||||
|
RegisterResponse registerResponse = new RegisterResponse(user, token);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", true,
|
||||||
|
"message", "User registered successfully",
|
||||||
|
"data", registerResponse
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
String message;
|
||||||
|
if (e.getMessage().contains("already exists")) {
|
||||||
|
message = "A user with this phone number already exists";
|
||||||
|
} else if (e.getMessage().contains("Invalid or expired")) {
|
||||||
|
message = "Invalid or expired invitation code";
|
||||||
|
} else {
|
||||||
|
message = "Registration failed. Please try again";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", false,
|
||||||
|
"message", message
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/admin/createUser")
|
@PostMapping("/admin/createUser")
|
||||||
@Operation(summary = "Create new user (Admin only)", description = "Creates a new user - requires admin privileges")
|
@Operation(summary = "Create new user (Admin only)", description = "Creates a new user - requires admin privileges")
|
||||||
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request) {
|
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request) {
|
||||||
try {
|
try {
|
||||||
User user = userService.createUser(request.getEmail(), request.getPassword(), request.getDisplayName());
|
User user = userService.createUser(request.getPhoneNumber(), request.getPassword(), request.getDisplayName());
|
||||||
return ResponseEntity.ok(user);
|
return ResponseEntity.ok(user);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/admin/createInvitationCode")
|
||||||
|
@Operation(summary = "Create invitation code (Admin only)", description = "Generates a new invitation code - requires admin privileges")
|
||||||
|
public ResponseEntity<Map<String, Object>> createInvitationCode() {
|
||||||
|
try {
|
||||||
|
InvitationCode invitationCode = invitationCodeService.generateInvitationCode();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", true,
|
||||||
|
"message", "Invitation code created successfully",
|
||||||
|
"data", invitationCode
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
String message;
|
||||||
|
if (e.getMessage().contains("Unable to generate unique")) {
|
||||||
|
message = "Failed to generate unique invitation code. Please try again.";
|
||||||
|
} else {
|
||||||
|
message = "Failed to create invitation code. Please try again.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", false,
|
||||||
|
"message", message
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -51,8 +51,8 @@ public class InvitationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String userEmail = authentication.getName();
|
String userPhoneNumber = authentication.getName();
|
||||||
InvitationResponse response = invitationService.createInvitation(request, userEmail);
|
InvitationResponse response = invitationService.createInvitation(request, userPhoneNumber);
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
String message;
|
String message;
|
||||||
@ -87,8 +87,8 @@ public class InvitationController {
|
|||||||
@Operation(summary = "Get all categorized invitations", description = "Get invitations in three categories: created, accepted, and available")
|
@Operation(summary = "Get all categorized invitations", description = "Get invitations in three categories: created, accepted, and available")
|
||||||
public ResponseEntity<ApiResponse<CategorizedInvitationsResponse>> getAllCategorizedInvitations(Authentication authentication) {
|
public ResponseEntity<ApiResponse<CategorizedInvitationsResponse>> getAllCategorizedInvitations(Authentication authentication) {
|
||||||
try {
|
try {
|
||||||
String userEmail = authentication.getName();
|
String userPhoneNumber = authentication.getName();
|
||||||
CategorizedInvitationsResponse response = invitationService.getAllCategorizedInvitations(userEmail);
|
CategorizedInvitationsResponse response = invitationService.getAllCategorizedInvitations(userPhoneNumber);
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||||
@ -104,8 +104,8 @@ public class InvitationController {
|
|||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String userEmail = authentication.getName();
|
String userPhoneNumber = authentication.getName();
|
||||||
invitationService.acceptInvitation(request.getId(), userEmail);
|
invitationService.acceptInvitation(request.getId(), userPhoneNumber);
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true));
|
return ResponseEntity.ok(new ApiResponse<>(true));
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||||
@ -121,8 +121,8 @@ public class InvitationController {
|
|||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String userEmail = authentication.getName();
|
String userPhoneNumber = authentication.getName();
|
||||||
invitationService.cancelInvitation(request.getId(), userEmail);
|
invitationService.cancelInvitation(request.getId(), userPhoneNumber);
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true));
|
return ResponseEntity.ok(new ApiResponse<>(true));
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||||
|
|||||||
@ -89,7 +89,7 @@ public class PostController {
|
|||||||
Long targetUserId;
|
Long targetUserId;
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
// Use authenticated user's ID when no id is provided
|
// Use authenticated user's ID when no id is provided
|
||||||
String userEmail = authentication.getName();
|
String userPhoneNumber = authentication.getName();
|
||||||
targetUserId = userService.getCurrentUser().getId();
|
targetUserId = userService.getCurrentUser().getId();
|
||||||
} else {
|
} else {
|
||||||
if (id <= 0) {
|
if (id <= 0) {
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public class CheckInvitationRequest {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
public CheckInvitationRequest() {}
|
||||||
|
|
||||||
|
public CheckInvitationRequest(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCode(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,14 @@
|
|||||||
package online.wesal.wesal.dto;
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.Email;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
public class CreateUserRequest {
|
public class CreateUserRequest {
|
||||||
|
|
||||||
@Email
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String email;
|
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "Phone number must be in international format starting with + and country code")
|
||||||
|
private String phoneNumber;
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Size(min = 6)
|
@Size(min = 6)
|
||||||
@ -19,18 +19,18 @@ public class CreateUserRequest {
|
|||||||
|
|
||||||
public CreateUserRequest() {}
|
public CreateUserRequest() {}
|
||||||
|
|
||||||
public CreateUserRequest(String email, String password, String displayName) {
|
public CreateUserRequest(String phoneNumber, String password, String displayName) {
|
||||||
this.email = email;
|
this.phoneNumber = phoneNumber;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEmail() {
|
public String getPhoneNumber() {
|
||||||
return email;
|
return phoneNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEmail(String email) {
|
public void setPhoneNumber(String phoneNumber) {
|
||||||
this.email = email;
|
this.phoneNumber = phoneNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPassword() {
|
public String getPassword() {
|
||||||
|
|||||||
@ -5,24 +5,24 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
public class LoginRequest {
|
public class LoginRequest {
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String emailOrUsername;
|
private String phoneNumberOrUsername;
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
public LoginRequest() {}
|
public LoginRequest() {}
|
||||||
|
|
||||||
public LoginRequest(String emailOrUsername, String password) {
|
public LoginRequest(String phoneNumberOrUsername, String password) {
|
||||||
this.emailOrUsername = emailOrUsername;
|
this.phoneNumberOrUsername = phoneNumberOrUsername;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEmailOrUsername() {
|
public String getPhoneNumberOrUsername() {
|
||||||
return emailOrUsername;
|
return phoneNumberOrUsername;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEmailOrUsername(String emailOrUsername) {
|
public void setPhoneNumberOrUsername(String phoneNumberOrUsername) {
|
||||||
this.emailOrUsername = emailOrUsername;
|
this.phoneNumberOrUsername = phoneNumberOrUsername;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPassword() {
|
public String getPassword() {
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
public class RegisterRequest {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "Phone number must be in international format starting with + and country code")
|
||||||
|
private String phoneNumber;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(min = 6)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
public RegisterRequest() {}
|
||||||
|
|
||||||
|
public RegisterRequest(String code, String phoneNumber, String password) {
|
||||||
|
this.code = code;
|
||||||
|
this.phoneNumber = phoneNumber;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCode(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneNumber() {
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneNumber(String phoneNumber) {
|
||||||
|
this.phoneNumber = phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.User;
|
||||||
|
|
||||||
|
public class RegisterResponse {
|
||||||
|
|
||||||
|
private User user;
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
public RegisterResponse() {}
|
||||||
|
|
||||||
|
public RegisterResponse(User user, String token) {
|
||||||
|
this.user = user;
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(User user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package online.wesal.wesal.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "invitation_codes")
|
||||||
|
public class InvitationCode {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(unique = true, nullable = false)
|
||||||
|
@NotBlank
|
||||||
|
@Size(max = 200)
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime expirationDate;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdDate;
|
||||||
|
|
||||||
|
public InvitationCode() {
|
||||||
|
this.createdDate = LocalDateTime.now();
|
||||||
|
this.expirationDate = LocalDateTime.now().plusDays(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvitationCode(String code) {
|
||||||
|
this();
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCode(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getExpirationDate() {
|
||||||
|
return expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpirationDate(LocalDateTime expirationDate) {
|
||||||
|
this.expirationDate = expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedDate() {
|
||||||
|
return createdDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedDate(LocalDateTime createdDate) {
|
||||||
|
this.createdDate = createdDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
return LocalDateTime.now().isAfter(this.expirationDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
package online.wesal.wesal.entity;
|
package online.wesal.wesal.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import jakarta.validation.constraints.Email;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@ -19,9 +19,9 @@ public class User {
|
|||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
@Email
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String email;
|
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "Phone number must be in international format starting with + and country code")
|
||||||
|
private String phoneNumber;
|
||||||
|
|
||||||
@Column(unique = true)
|
@Column(unique = true)
|
||||||
private String username;
|
private String username;
|
||||||
@ -46,12 +46,12 @@ public class User {
|
|||||||
private String role = "USER";
|
private String role = "USER";
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String subscriptions = "[\"newinvites\",\"invitesfollowup\",\"appnews\"]";
|
private String subscriptions = "[\"postcomments\",\"newinvites\",\"invitesfollowup\",\"appnews\"]";
|
||||||
|
|
||||||
public User() {}
|
public User() {}
|
||||||
|
|
||||||
public User(String email, String password, String displayName) {
|
public User(String phoneNumber, String password, String displayName) {
|
||||||
this.email = email;
|
this.phoneNumber = phoneNumber;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
}
|
}
|
||||||
@ -64,12 +64,12 @@ public class User {
|
|||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEmail() {
|
public String getPhoneNumber() {
|
||||||
return email;
|
return phoneNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEmail(String email) {
|
public void setPhoneNumber(String phoneNumber) {
|
||||||
this.email = email;
|
this.phoneNumber = phoneNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
@ -130,13 +130,13 @@ public class User {
|
|||||||
|
|
||||||
public List<String> getSubscriptions() {
|
public List<String> getSubscriptions() {
|
||||||
if (subscriptions == null || subscriptions.isEmpty()) {
|
if (subscriptions == null || subscriptions.isEmpty()) {
|
||||||
return Arrays.asList("newinvites", "invitesfollowup", "appnews");
|
return Arrays.asList("postcomments", "newinvites", "invitesfollowup", "appnews");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
return mapper.readValue(subscriptions, new TypeReference<List<String>>() {});
|
return mapper.readValue(subscriptions, new TypeReference<List<String>>() {});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Arrays.asList("newinvites", "invitesfollowup", "appnews");
|
return Arrays.asList("postcomments", "newinvites", "invitesfollowup", "appnews");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ public class User {
|
|||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
this.subscriptions = mapper.writeValueAsString(subscriptions);
|
this.subscriptions = mapper.writeValueAsString(subscriptions);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
this.subscriptions = "[\"newinvites\",\"invitesfollowup\",\"appnews\"]";
|
this.subscriptions = "[\"postcomments\",\"newinvites\",\"invitesfollowup\",\"appnews\"]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package online.wesal.wesal.repository;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.InvitationCode;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface InvitationCodeRepository extends JpaRepository<InvitationCode, Long> {
|
||||||
|
Optional<InvitationCode> findByCode(String code);
|
||||||
|
boolean existsByCode(String code);
|
||||||
|
|
||||||
|
@Query("SELECT ic FROM InvitationCode ic WHERE ic.code = :code AND ic.expirationDate > :currentTime")
|
||||||
|
Optional<InvitationCode> findValidCodeByCode(@Param("code") String code, @Param("currentTime") LocalDateTime currentTime);
|
||||||
|
}
|
||||||
@ -11,9 +11,9 @@ import java.util.Optional;
|
|||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
Optional<User> findByEmail(String email);
|
Optional<User> findByPhoneNumber(String phoneNumber);
|
||||||
Optional<User> findByUsername(String username);
|
Optional<User> findByUsername(String username);
|
||||||
boolean existsByEmail(String email);
|
boolean existsByPhoneNumber(String phoneNumber);
|
||||||
boolean existsByUsername(String username);
|
boolean existsByUsername(String username);
|
||||||
|
|
||||||
@Query("SELECT u FROM User u WHERE u.subscriptions LIKE %:subscription% AND u.fcmToken IS NOT NULL AND u.fcmToken != ''")
|
@Query("SELECT u FROM User u WHERE u.subscriptions LIKE %:subscription% AND u.fcmToken IS NOT NULL AND u.fcmToken != ''")
|
||||||
|
|||||||
@ -21,23 +21,23 @@ public class AuthService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private JwtUtil jwtUtil;
|
private JwtUtil jwtUtil;
|
||||||
|
|
||||||
public String authenticate(String emailOrUsername, String password) {
|
public String authenticate(String phoneNumberOrUsername, String password) {
|
||||||
Optional<User> userOpt = userRepository.findByEmail(emailOrUsername);
|
Optional<User> userOpt = userRepository.findByPhoneNumber(phoneNumberOrUsername);
|
||||||
|
|
||||||
if (userOpt.isEmpty()) {
|
if (userOpt.isEmpty()) {
|
||||||
userOpt = userRepository.findByUsername(emailOrUsername);
|
userOpt = userRepository.findByUsername(phoneNumberOrUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
System.out.println("Authenticating user: " + user.getEmail());
|
System.out.println("Authenticating user: " + user.getPhoneNumber());
|
||||||
if (passwordEncoder.matches(password, user.getPassword())) {
|
if (passwordEncoder.matches(password, user.getPassword())) {
|
||||||
System.out.println("Password matches!");
|
System.out.println("Password matches!");
|
||||||
if (!user.isActivated()) {
|
if (!user.isActivated()) {
|
||||||
user.setActivated(true);
|
user.setActivated(true);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
return jwtUtil.generateToken(user.getEmail());
|
return jwtUtil.generateToken(user.getPhoneNumber());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
package online.wesal.wesal.service;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.InvitationCode;
|
||||||
|
import online.wesal.wesal.entity.User;
|
||||||
|
import online.wesal.wesal.repository.InvitationCodeRepository;
|
||||||
|
import online.wesal.wesal.repository.UserRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class InvitationCodeService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InvitationCodeRepository invitationCodeRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
public boolean isValidCode(String code) {
|
||||||
|
Optional<InvitationCode> invitationCodeOpt = invitationCodeRepository.findValidCodeByCode(code, LocalDateTime.now());
|
||||||
|
return invitationCodeOpt.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<InvitationCode> findValidCode(String code) {
|
||||||
|
return invitationCodeRepository.findValidCodeByCode(code, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User registerUserWithCode(String code, String phoneNumber, String password) {
|
||||||
|
// Check if phone number already exists
|
||||||
|
if (userRepository.existsByPhoneNumber(phoneNumber)) {
|
||||||
|
throw new RuntimeException("User with this phone number already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate invitation code
|
||||||
|
Optional<InvitationCode> invitationCodeOpt = findValidCode(code);
|
||||||
|
if (invitationCodeOpt.isEmpty()) {
|
||||||
|
throw new RuntimeException("Invalid or expired invitation code");
|
||||||
|
}
|
||||||
|
|
||||||
|
InvitationCode invitationCode = invitationCodeOpt.get();
|
||||||
|
|
||||||
|
// Create new user
|
||||||
|
User user = new User();
|
||||||
|
user.setPhoneNumber(phoneNumber);
|
||||||
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
|
user.setDisplayName("Guest");
|
||||||
|
user.setRole("USER");
|
||||||
|
user.setActivated(false);
|
||||||
|
user.setSubscriptions(Arrays.asList("postcomments", "newinvites", "invitesfollowup", "appnews"));
|
||||||
|
|
||||||
|
// Save user
|
||||||
|
User savedUser = userRepository.save(user);
|
||||||
|
|
||||||
|
// Remove (delete) the invitation code so it can't be used again
|
||||||
|
invitationCodeRepository.delete(invitationCode);
|
||||||
|
|
||||||
|
return savedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvitationCode generateInvitationCode() {
|
||||||
|
String code;
|
||||||
|
int attempts = 0;
|
||||||
|
int maxAttempts = 100;
|
||||||
|
|
||||||
|
// Generate a unique 6-character code
|
||||||
|
do {
|
||||||
|
code = generateRandomCode();
|
||||||
|
attempts++;
|
||||||
|
if (attempts > maxAttempts) {
|
||||||
|
throw new RuntimeException("Unable to generate unique invitation code after " + maxAttempts + " attempts");
|
||||||
|
}
|
||||||
|
} while (invitationCodeRepository.existsByCode(code));
|
||||||
|
|
||||||
|
InvitationCode invitationCode = new InvitationCode(code);
|
||||||
|
return invitationCodeRepository.save(invitationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateRandomCode() {
|
||||||
|
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
Random random = new Random();
|
||||||
|
StringBuilder code = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
code.append(characters.charAt(random.nextInt(characters.length())));
|
||||||
|
}
|
||||||
|
|
||||||
|
return code.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -42,8 +42,8 @@ public class InvitationService {
|
|||||||
private SubscriptionNotificationService subscriptionNotificationService;
|
private SubscriptionNotificationService subscriptionNotificationService;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public InvitationResponse createInvitation(CreateInvitationRequest request, String userEmail) {
|
public InvitationResponse createInvitation(CreateInvitationRequest request, String userPhoneNumber) {
|
||||||
User creator = userRepository.findByEmail(userEmail)
|
User creator = userRepository.findByPhoneNumber(userPhoneNumber)
|
||||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
Tag tag = tagRepository.findById(request.getTagId())
|
Tag tag = tagRepository.findById(request.getTagId())
|
||||||
@ -85,8 +85,8 @@ public class InvitationService {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public CategorizedInvitationsResponse getAllCategorizedInvitations(String userEmail) {
|
public CategorizedInvitationsResponse getAllCategorizedInvitations(String userPhoneNumber) {
|
||||||
User user = userRepository.findByEmail(userEmail)
|
User user = userRepository.findByPhoneNumber(userPhoneNumber)
|
||||||
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
|
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
|
||||||
|
|
||||||
List<InvitationResponse> created = invitationRepository.findByCreatorIdOrderByCreationDateDesc(user.getId())
|
List<InvitationResponse> created = invitationRepository.findByCreatorIdOrderByCreationDateDesc(user.getId())
|
||||||
@ -110,8 +110,8 @@ public class InvitationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void acceptInvitation(Long invitationId, String userEmail) {
|
public void acceptInvitation(Long invitationId, String userPhoneNumber) {
|
||||||
User user = userRepository.findByEmail(userEmail)
|
User user = userRepository.findByPhoneNumber(userPhoneNumber)
|
||||||
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
|
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
|
||||||
|
|
||||||
Invitation invitation = invitationRepository.findById(invitationId)
|
Invitation invitation = invitationRepository.findById(invitationId)
|
||||||
@ -171,8 +171,8 @@ public class InvitationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void cancelInvitation(Long invitationId, String userEmail) {
|
public void cancelInvitation(Long invitationId, String userPhoneNumber) {
|
||||||
User user = userRepository.findByEmail(userEmail)
|
User user = userRepository.findByPhoneNumber(userPhoneNumber)
|
||||||
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
|
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
|
||||||
|
|
||||||
Invitation invitation = invitationRepository.findById(invitationId)
|
Invitation invitation = invitationRepository.findById(invitationId)
|
||||||
|
|||||||
@ -20,15 +20,15 @@ public class UserDetailsServiceImpl implements UserDetailsService {
|
|||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String phoneNumber) throws UsernameNotFoundException {
|
||||||
User user = userRepository.findByEmail(email)
|
User user = userRepository.findByPhoneNumber(phoneNumber)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + phoneNumber));
|
||||||
|
|
||||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
|
authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
|
||||||
|
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getEmail(),
|
user.getPhoneNumber(),
|
||||||
user.getPassword(),
|
user.getPassword(),
|
||||||
authorities
|
authorities
|
||||||
);
|
);
|
||||||
|
|||||||
@ -20,8 +20,8 @@ public class UserService {
|
|||||||
|
|
||||||
public User getCurrentUser() {
|
public User getCurrentUser() {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
String email = authentication.getName();
|
String phoneNumber = authentication.getName();
|
||||||
return userRepository.findByEmail(email)
|
return userRepository.findByPhoneNumber(phoneNumber)
|
||||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,16 +44,16 @@ public class UserService {
|
|||||||
return "ADMIN".equals(user.getRole());
|
return "ADMIN".equals(user.getRole());
|
||||||
}
|
}
|
||||||
|
|
||||||
public User createUser(String email, String password, String displayName) {
|
public User createUser(String phoneNumber, String password, String displayName) {
|
||||||
if (!isCurrentUserAdmin()) {
|
if (!isCurrentUserAdmin()) {
|
||||||
throw new RuntimeException("Access denied: Admin privileges required");
|
throw new RuntimeException("Access denied: Admin privileges required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userRepository.existsByEmail(email)) {
|
if (userRepository.existsByPhoneNumber(phoneNumber)) {
|
||||||
throw new RuntimeException("User with this email already exists");
|
throw new RuntimeException("User with this phone number already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = new User(email, passwordEncoder.encode(password), displayName);
|
User user = new User(phoneNumber, passwordEncoder.encode(password), displayName);
|
||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ class ApiConstants {
|
|||||||
|
|
||||||
// Auth endpoints
|
// Auth endpoints
|
||||||
static const String loginEndpoint = '/login';
|
static const String loginEndpoint = '/login';
|
||||||
|
static const String registerEndpoint = '/register';
|
||||||
|
static const String checkInvitationEndpoint = '/checkInvitation';
|
||||||
|
|
||||||
// User endpoints
|
// User endpoints
|
||||||
static const String getUserEndpoint = '/getUser';
|
static const String getUserEndpoint = '/getUser';
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import 'screens/edit_profile_screen.dart';
|
|||||||
import 'services/auth_service.dart';
|
import 'services/auth_service.dart';
|
||||||
import 'services/user_service.dart';
|
import 'services/user_service.dart';
|
||||||
import 'services/app_lifecycle_service.dart';
|
import 'services/app_lifecycle_service.dart';
|
||||||
|
import 'services/http_service.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
@ -260,10 +262,14 @@ class SignInPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _SignInPageState extends State<SignInPage> {
|
class _SignInPageState extends State<SignInPage> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _emailController = TextEditingController();
|
final _phoneController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
|
final _invitationCodeController = TextEditingController();
|
||||||
|
final _countryCodeController = TextEditingController(text: '+966');
|
||||||
bool _isPasswordVisible = false;
|
bool _isPasswordVisible = false;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
bool _isRegistering = false;
|
||||||
|
int _registrationStep = 1; // 1 = invitation code, 2 = phone & password
|
||||||
|
|
||||||
void _showHelpBottomSheet() {
|
void _showHelpBottomSheet() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
@ -297,7 +303,7 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'For account creation or password reset, please contact ERP Management Group from your Aramco email address.',
|
'Registration is by invitation only. If you don\'t have an invitation code, please contact COD/DPSD.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@ -331,10 +337,51 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInvitationCode() async {
|
||||||
|
if (_invitationCodeController.text.trim().isEmpty) {
|
||||||
|
_showErrorAlert('Please enter your invitation code');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await HttpService.post('/checkInvitation', {
|
||||||
|
'code': _invitationCodeController.text.trim().toUpperCase(),
|
||||||
|
});
|
||||||
|
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data['status'] == true &&
|
||||||
|
data['data'] != null &&
|
||||||
|
data['data']['valid'] == true) {
|
||||||
|
// Invitation code is valid, move to step 2
|
||||||
|
setState(() {
|
||||||
|
_registrationStep = 2;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_showErrorAlert(data['message'] ?? 'Invalid invitation code');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_showErrorAlert('Network error. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_emailController.dispose();
|
_phoneController.dispose();
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
|
_invitationCodeController.dispose();
|
||||||
|
_countryCodeController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,8 +391,10 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final phoneNumber =
|
||||||
|
_countryCodeController.text.trim() + _phoneController.text.trim();
|
||||||
final result = await AuthService.login(
|
final result = await AuthService.login(
|
||||||
_emailController.text.trim(),
|
phoneNumber,
|
||||||
_passwordController.text,
|
_passwordController.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -386,11 +435,83 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleRegister() async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final phoneNumber =
|
||||||
|
_countryCodeController.text.trim() + _phoneController.text.trim();
|
||||||
|
|
||||||
|
final response = await HttpService.post('/register', {
|
||||||
|
'code': _invitationCodeController.text.trim().toUpperCase(),
|
||||||
|
'phoneNumber': phoneNumber,
|
||||||
|
'password': _passwordController.text,
|
||||||
|
});
|
||||||
|
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data['status'] == true && data['data'] != null) {
|
||||||
|
// Registration successful, save token and navigate
|
||||||
|
final token = data['data']['token'];
|
||||||
|
if (token != null) {
|
||||||
|
await AuthService.saveToken(token);
|
||||||
|
|
||||||
|
final userResult = await UserService.getCurrentUser(
|
||||||
|
forceRefresh: true,
|
||||||
|
);
|
||||||
|
if (userResult['success'] == true) {
|
||||||
|
final userData = userResult['data'];
|
||||||
|
|
||||||
|
// Check if user needs onboarding (activated = false)
|
||||||
|
if (userData['activated'] == false) {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => EditProfileScreen(
|
||||||
|
userData: userData,
|
||||||
|
isOnboarding: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
AppLifecycleService.startAllPolling();
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => NotificationPermissionScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_showErrorAlert(
|
||||||
|
'Registration successful but failed to load user data',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_showErrorAlert('Registration successful but no token received');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_showErrorAlert(data['message'] ?? 'Registration failed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_showErrorAlert('Network error. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _showErrorAlert(String message) {
|
void _showErrorAlert(String message) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text('Login Failed'),
|
title: Text(_isRegistering ? 'Registration' : 'Login Failed'),
|
||||||
content: Text(message),
|
content: Text(message),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@ -430,7 +551,7 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
icon: Icon(Icons.arrow_back, color: Colors.white),
|
icon: Icon(Icons.arrow_back, color: Colors.white),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Sign In',
|
_isRegistering ? 'Register' : 'Sign In',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -482,7 +603,9 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
|
|
||||||
// Welcome text
|
// Welcome text
|
||||||
Text(
|
Text(
|
||||||
'Welcome Back!',
|
_isRegistering
|
||||||
|
? 'Create Account'
|
||||||
|
: 'Welcome Back!',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -494,7 +617,9 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
'Sign in to socialize with your colleagues\nand transform your social life!',
|
_isRegistering
|
||||||
|
? 'Join with an invitation code to connect\nwith your colleagues!'
|
||||||
|
: 'Sign in to socialize with your colleagues\nand transform your social life!',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
@ -504,13 +629,15 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
|
|
||||||
SizedBox(height: 40),
|
SizedBox(height: 40),
|
||||||
|
|
||||||
// Email field
|
// Registration Step 1: Invitation Code
|
||||||
|
if (_isRegistering && _registrationStep == 1) ...[
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _invitationCodeController,
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Email',
|
labelText: 'Invitation Code',
|
||||||
prefixIcon: Icon(Icons.email_outlined),
|
prefixIcon: Icon(
|
||||||
|
Icons.confirmation_number_outlined,
|
||||||
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
@ -520,21 +647,107 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
color: Color(0xFF6A4C93),
|
color: Color(0xFF6A4C93),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
helperText:
|
||||||
|
'6-character code with letters and numbers',
|
||||||
),
|
),
|
||||||
|
maxLength: 6,
|
||||||
|
textCapitalization:
|
||||||
|
TextCapitalization.characters,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Please enter your email';
|
return 'Please enter your invitation code';
|
||||||
}
|
}
|
||||||
if (!value.contains('@')) {
|
if (value.length != 6) {
|
||||||
return 'Please enter a valid email';
|
return 'Invitation code must be 6 characters';
|
||||||
|
}
|
||||||
|
if (!RegExp(
|
||||||
|
r'^[A-Z0-9]{6}$',
|
||||||
|
).hasMatch(value)) {
|
||||||
|
return 'Code must contain only letters and numbers';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Login or Registration Step 2: Phone & Password
|
||||||
|
if (!_isRegistering || _registrationStep == 2) ...[
|
||||||
|
// Phone number field with country code input
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _countryCodeController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Code',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Required';
|
||||||
|
}
|
||||||
|
if (!value.startsWith('+')) {
|
||||||
|
return 'Must start with +';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _phoneController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Phone Number',
|
||||||
|
prefixIcon: Icon(Icons.phone_outlined),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter your phone number';
|
||||||
|
}
|
||||||
|
if (value.length < 8) {
|
||||||
|
return 'Please enter a valid phone number';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Password field (only for login or registration step 2)
|
||||||
|
if (!_isRegistering || _registrationStep == 2) ...[
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
|
|
||||||
// Password field
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: !_isPasswordVisible,
|
obscureText: !_isPasswordVisible,
|
||||||
@ -549,7 +762,8 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isPasswordVisible = !_isPasswordVisible;
|
_isPasswordVisible =
|
||||||
|
!_isPasswordVisible;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -573,10 +787,12 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
|
||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
|
|
||||||
// Forgot password
|
// Forgot password (only show for login)
|
||||||
|
if (!_isRegistering)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
@ -593,11 +809,21 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
|
|
||||||
SizedBox(height: 30),
|
SizedBox(height: 30),
|
||||||
|
|
||||||
// Sign in button
|
// Action button based on current state
|
||||||
Container(
|
Container(
|
||||||
height: 56,
|
height: 56,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isLoading ? null : _handleSignIn,
|
onPressed: _isLoading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (!_isRegistering) {
|
||||||
|
_handleSignIn();
|
||||||
|
} else if (_registrationStep == 1) {
|
||||||
|
_checkInvitationCode();
|
||||||
|
} else {
|
||||||
|
_handleRegister();
|
||||||
|
}
|
||||||
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
@ -614,7 +840,11 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
'Sign In',
|
!_isRegistering
|
||||||
|
? 'Sign In'
|
||||||
|
: (_registrationStep == 1
|
||||||
|
? 'Continue'
|
||||||
|
: 'Register'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -623,20 +853,65 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Back button for registration step 2
|
||||||
|
if (_isRegistering && _registrationStep == 2) ...[
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
|
height: 56,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_registrationStep = 1;
|
||||||
|
_phoneController.clear();
|
||||||
|
_passwordController.clear();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: BorderSide(color: Color(0xFF6A4C93)),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Back to Invitation Code',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
|
|
||||||
// Contact link for new accounts
|
// Toggle between login and registration (only show on step 1)
|
||||||
|
if (!_isRegistering || _registrationStep == 1)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Don't have an account? ",
|
_isRegistering
|
||||||
|
? 'Already have an account? '
|
||||||
|
: "Don't have an account? ",
|
||||||
style: TextStyle(color: Colors.grey[600]),
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _showHelpBottomSheet,
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_isRegistering = !_isRegistering;
|
||||||
|
_registrationStep = 1;
|
||||||
|
// Clear form when switching modes
|
||||||
|
_phoneController.clear();
|
||||||
|
_passwordController.clear();
|
||||||
|
_invitationCodeController.clear();
|
||||||
|
});
|
||||||
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'Contact Support',
|
_isRegistering
|
||||||
|
? 'Sign In'
|
||||||
|
: 'Register Now!',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF6A4C93),
|
color: Color(0xFF6A4C93),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -646,6 +921,19 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
if (_isRegistering) ...[
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Registration is only by invitation.\nIf you don\'t have an invite, contact COD/DPSD.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
SizedBox(height: 40),
|
SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
368
frontend/lib/screens/create_invitation_screen.dart
Normal file
368
frontend/lib/screens/create_invitation_screen.dart
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
import '../services/http_service.dart';
|
||||||
|
import '../constants/api_constants.dart';
|
||||||
|
|
||||||
|
class CreateInvitationScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_CreateInvitationScreenState createState() => _CreateInvitationScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateInvitationScreenState extends State<CreateInvitationScreen> {
|
||||||
|
bool _isLoading = false;
|
||||||
|
Map<String, dynamic>? _createdInvitation;
|
||||||
|
|
||||||
|
Future<void> _createInvitationCode() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await HttpService.post(ApiConstants.createInvitationCodeEndpoint, {});
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data['status'] == true && data['data'] != null) {
|
||||||
|
setState(() {
|
||||||
|
_createdInvitation = data['data'];
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(data['message'] ?? 'Invitation code created successfully'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
duration: Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_showErrorAlert(data['message'] ?? 'Failed to create invitation code');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_showErrorAlert('Network error. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorAlert(String message) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('Error'),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('OK', style: TextStyle(color: Color(0xFF6A4C93))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _copyToClipboard(String text) async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: text));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Copied to clipboard'),
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDate(String dateString) {
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(dateString);
|
||||||
|
return '${date.day}/${date.month}/${date.year} at ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||||
|
} catch (e) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
'Create Invitation Code',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [Color(0xFF6A4C93), Colors.white],
|
||||||
|
stops: [0.0, 0.3],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.confirmation_number_outlined,
|
||||||
|
size: 48,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Generate New Invitation Code',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Create a new invitation code for user registration',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Create button
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _createInvitationCode,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.add, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Create New Invitation Code',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Created invitation code display
|
||||||
|
if (_createdInvitation != null) ...[
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Invitation Code Created',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Invitation code
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Color(0xFF6A4C93), width: 2),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_createdInvitation!['code'] ?? 'N/A',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
letterSpacing: 2.0,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _copyToClipboard(_createdInvitation!['code'] ?? ''),
|
||||||
|
icon: Icon(
|
||||||
|
Icons.copy,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
tooltip: 'Copy to clipboard',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Details
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildDetailRow(
|
||||||
|
'ID:',
|
||||||
|
_createdInvitation!['id']?.toString() ?? 'N/A',
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildDetailRow(
|
||||||
|
'Created:',
|
||||||
|
_formatDate(_createdInvitation!['createdDate'] ?? ''),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildDetailRow(
|
||||||
|
'Expires:',
|
||||||
|
_formatDate(_createdInvitation!['expirationDate'] ?? ''),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildDetailRow(
|
||||||
|
'Status:',
|
||||||
|
_createdInvitation!['expired'] == false ? 'Active' : 'Expired',
|
||||||
|
valueColor: _createdInvitation!['expired'] == false ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Copy full details button
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
final details = '''
|
||||||
|
Invitation Code: ${_createdInvitation!['code']}
|
||||||
|
ID: ${_createdInvitation!['id']}
|
||||||
|
Created: ${_formatDate(_createdInvitation!['createdDate'] ?? '')}
|
||||||
|
Expires: ${_formatDate(_createdInvitation!['expirationDate'] ?? '')}
|
||||||
|
Status: ${_createdInvitation!['expired'] == false ? 'Active' : 'Expired'}
|
||||||
|
''';
|
||||||
|
_copyToClipboard(details);
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.copy_all),
|
||||||
|
label: Text('Copy All Details'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: BorderSide(color: Color(0xFF6A4C93)),
|
||||||
|
foregroundColor: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailRow(String label, String value, {Color? valueColor}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: valueColor ?? Colors.black87,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final TextEditingController _displayNameController = TextEditingController();
|
final TextEditingController _displayNameController = TextEditingController();
|
||||||
final TextEditingController _usernameController = TextEditingController();
|
final TextEditingController _usernameController = TextEditingController();
|
||||||
final TextEditingController _emailController = TextEditingController();
|
final TextEditingController _phoneController = TextEditingController();
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -33,7 +33,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
void _initializeFields() {
|
void _initializeFields() {
|
||||||
if (widget.userData != null) {
|
if (widget.userData != null) {
|
||||||
_displayNameController.text = widget.userData!['displayName'] ?? '';
|
_displayNameController.text = widget.userData!['displayName'] ?? '';
|
||||||
_emailController.text = widget.userData!['email'] ?? '';
|
_phoneController.text = widget.userData!['phoneNumber'] ?? '';
|
||||||
if (!widget.isOnboarding) {
|
if (!widget.isOnboarding) {
|
||||||
_usernameController.text = widget.userData!['username'] ?? '';
|
_usernameController.text = widget.userData!['username'] ?? '';
|
||||||
}
|
}
|
||||||
@ -44,7 +44,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_displayNameController.dispose();
|
_displayNameController.dispose();
|
||||||
_usernameController.dispose();
|
_usernameController.dispose();
|
||||||
_emailController.dispose();
|
_phoneController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,11 +299,11 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _phoneController,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Email address',
|
hintText: 'Phone number',
|
||||||
prefixIcon: Icon(Icons.email, color: Colors.grey[400]),
|
prefixIcon: Icon(Icons.phone, color: Colors.grey[400]),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||||
@ -328,7 +328,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Email cannot be changed',
|
'Phone number cannot be changed',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import '../edit_profile_screen.dart';
|
|||||||
import '../support_screen.dart';
|
import '../support_screen.dart';
|
||||||
import '../information_screen.dart';
|
import '../information_screen.dart';
|
||||||
import '../notifications_settings_screen.dart';
|
import '../notifications_settings_screen.dart';
|
||||||
|
import '../create_invitation_screen.dart';
|
||||||
|
|
||||||
class ProfilePage extends StatefulWidget {
|
class ProfilePage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -752,9 +753,11 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 56,
|
height: 56,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () => _showCreateAccountDialog(context),
|
onPressed: () => Navigator.of(context).push(
|
||||||
icon: Icon(Icons.person_add, size: 20),
|
MaterialPageRoute(builder: (context) => CreateInvitationScreen()),
|
||||||
label: Text('Create an Account'),
|
),
|
||||||
|
icon: Icon(Icons.confirmation_number_outlined, size: 20),
|
||||||
|
label: Text('Create Invitation Code'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class AuthService {
|
|||||||
static const String _userDataKey = 'user_data';
|
static const String _userDataKey = 'user_data';
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> login(
|
static Future<Map<String, dynamic>> login(
|
||||||
String emailOrUsername,
|
String phoneNumberOrUsername,
|
||||||
String password,
|
String password,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
@ -20,7 +20,7 @@ class AuthService {
|
|||||||
Uri.parse('${ApiConstants.baseUrl}${ApiConstants.loginEndpoint}'),
|
Uri.parse('${ApiConstants.baseUrl}${ApiConstants.loginEndpoint}'),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: jsonEncode({
|
body: jsonEncode({
|
||||||
'emailOrUsername': emailOrUsername,
|
'phoneNumberOrUsername': phoneNumberOrUsername,
|
||||||
'password': password,
|
'password': password,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -57,6 +57,10 @@ class AuthService {
|
|||||||
return await _storage.read(key: _tokenKey);
|
return await _storage.read(key: _tokenKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> saveToken(String token) async {
|
||||||
|
await _storage.write(key: _tokenKey, value: token);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<bool> isLoggedIn() async {
|
static Future<bool> isLoggedIn() async {
|
||||||
final token = await getToken();
|
final token = await getToken();
|
||||||
return token != null && token.isNotEmpty;
|
return token != null && token.isNotEmpty;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:googleapis_auth/auth_io.dart';
|
import 'package:googleapis_auth/auth_io.dart';
|
||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
|
||||||
class NotificationService {
|
class NotificationService {
|
||||||
static final NotificationService _instance = NotificationService._internal();
|
static final NotificationService _instance = NotificationService._internal();
|
||||||
@ -17,22 +18,8 @@ class NotificationService {
|
|||||||
// Firebase project configuration
|
// Firebase project configuration
|
||||||
static const String _projectId = 'wesalapp-bc676';
|
static const String _projectId = 'wesalapp-bc676';
|
||||||
|
|
||||||
// Service account credentials (JSON string)
|
// Service account credentials loaded from file
|
||||||
static const String _serviceAccountJson = '''
|
static String? _serviceAccountJson;
|
||||||
{
|
|
||||||
"type": "service_account",
|
|
||||||
"project_id": "wesalapp-bc676",
|
|
||||||
"private_key_id": "90f1ac73e8f8b59e5ad7b0caed638ffa57675d39",
|
|
||||||
"private_key": "-----BEGIN PRIVATE KEY-----\\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCe/p4CkGeDft2F\\nLMDNyjbU0g1mLQ4bhaqy2WOHr8BX7kHt/WlpB3WeaSUItW2cXFtXhI66mW7Ejlg7\\n/Los4YLpVXner4P5Vj7sOVrhD1Jt9qmdsEjfsTaKs0t06tfUE9PdFqBl6F5HG9B1\\nkwdV2mQfiurpTb+zUXLpfvMbGT1ny7gJSXhYpRaC6UJUy7DNsxeChviXy1tPGr/r\\nmIaENt0KAZzRSSCp0bzN+gKoAm6qyOGYJ9++HlGXTDYmkGPkdc0lbSWjA2/+K7zR\\nWQNy4oOXCstdHL4lMp87UT8lL1ZnntROELyTBUslmYywxXtZinkOgBrWeUifqLW4\\nbcHy6jJ5AgMBAAECggEACSBwjZEggAneSXDCOI3tC9Zq8nyPnMDVhaK49eb+0Y1Z\\nt4GedWr6M3exqohPnHQowiNX1hpMo3fQVNEzFrRzQVWow0Gr/7oVrpW0Q8sPXkSU\\ng/rElCKmENwt7q40aXYh6UUNPAxUrRxJoRYpi6IXsT/WMEJISNDaGdExv1J5leWi\\no8Op2AhREV/ukFpTnNfzfWKjXN+i3psNCYqZAdAh+a4ZJH0vNpiaCq6uVFw7HzdR\\nF2mMha+MYtp2VupzDJ8DkL4ExQl1KCOCllahzqVrhqmEhhhTfDxPOj5q24Hnfn1p\\npzR+fC8Ex0dGB/j+9jKjQyilo/LzEJdrPxt/9QUdiQKBgQDQ2L7sQsEIihVUq3lW\\n3Od2GNjnloTo24rNLl8hxwgl9EA2VnG50gZPGAkcUA2eeA23T6C6gPbm0QBsfqSP\\nPNTbd6UYF508EE7PMaScFoJMcKDin8x4q5tfVjgao2r8LCOUXfU1ktreQR3bIMKk\\nsgsgBazfBT84ioxvDwoD+4EJqwKBgQDC5HEfouCTxvKwzmpxZ+kkHKPO5QjyxnOH\\nLnt/7jx5t7T1nWNUnusYj+uowaqKAmLz7kBhCbRGADdKuBAr0hY/aEOG3zhTH35K\\nc+8wJ3yDFkh8BhFsOYCxopIPAjEGxw5cnM4+r8QDqy61j4hsR9kSr40WwhRuSxu+\\nHqe38Vl4awKBgBYFJGxIxY2e8YzR36NW+1iqWgRhDHZ433Ou1fz7vVIzJKoWBzuu\\nd1fTkvJXRnhU9C1Fyg6gFmhT1RWbbMJliZPyU4fsxXlVxtl1xINopChnH6+FZcu7\\nXFB7CMNWQ6t/A+la1sXlTApvFzTJiXxQAXhI4OdK6FWP1irHjSjKVdqtAoGAcLQA\\ngyYCrxKux/YmcfyAQ2TYic3DNfnzVypXOuz/RfgpipwAlC/ujl60Dfwo7fRhWuTd\\nkAA3ov9++hOlLmIogXR/EGDHxrIAq3eNy5AaHghl1GsB6k76kD8OLeW7ikrUkFQR\\npQip1uFIerBNWSjXbEne0llbzUhb+77oiKPmdI8CgYBRBIUC4d/T3cgmF9uRqlpL\\nSsa8IxqohU1huAucf5UqNP9OXZ4TtZhM0PbR5SNHcGBXAl+XowCtUeCE9LlWWzpg\\ne/xTu4Mu1hwnRZ8ybujAyTPnN8KEfK8HDjORZnxzzdyPkO4BN+KOH6WZyKhKDTuR\\n6KCch1ooA8YlV43vchpKXg==\\n-----END PRIVATE KEY-----\\n",
|
|
||||||
"client_email": "firebase-adminsdk-fbsvc@wesalapp-bc676.iam.gserviceaccount.com",
|
|
||||||
"client_id": "112586481303824467416",
|
|
||||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
||||||
"token_uri": "https://oauth2.googleapis.com/token",
|
|
||||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
||||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40wesalapp-bc676.iam.gserviceaccount.com",
|
|
||||||
"universe_domain": "googleapis.com"
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
|
|
||||||
static const List<String> topics = [
|
static const List<String> topics = [
|
||||||
'all',
|
'all',
|
||||||
@ -257,10 +244,30 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadServiceAccountCredentials() async {
|
||||||
|
if (_serviceAccountJson != null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
_serviceAccountJson = await rootBundle.loadString(
|
||||||
|
'firebase-service-account.json',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading service account credentials: $e');
|
||||||
|
_serviceAccountJson = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<String?> _getAccessToken() async {
|
Future<String?> _getAccessToken() async {
|
||||||
try {
|
try {
|
||||||
|
await _loadServiceAccountCredentials();
|
||||||
|
|
||||||
|
if (_serviceAccountJson == null) {
|
||||||
|
print('Service account credentials not loaded');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final credentials = ServiceAccountCredentials.fromJson(
|
final credentials = ServiceAccountCredentials.fromJson(
|
||||||
_serviceAccountJson,
|
_serviceAccountJson!,
|
||||||
);
|
);
|
||||||
final client = await clientViaServiceAccount(credentials, [
|
final client = await clientViaServiceAccount(credentials, [
|
||||||
'https://www.googleapis.com/auth/firebase.messaging',
|
'https://www.googleapis.com/auth/firebase.messaging',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user