Working on profile and push notification #30

Merged
RecentRunner merged 2 commits from WorkingOnProfileAndPushNotification into main 2026-03-25 09:18:49 -06:00
10 changed files with 197 additions and 47 deletions
Showing only changes of commit b46705396d - Show all commits

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>

View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

6
android/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -19,6 +19,7 @@ import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.AuthDTO;
import com.example.petstoremobile.dtos.UserDTO;
import retrofit2.Call;
import retrofit2.Callback;
@@ -90,11 +91,11 @@ public class MainActivity extends AppCompatActivity {
);
//fetch user id from api then login to home activity
RetrofitClient.getAuthApi(MainActivity.this).getCurrentUser()
.enqueue(new Callback<AuthDTO.UserResponse>() {
RetrofitClient.getAuthApi(MainActivity.this).getMe()
.enqueue(new Callback<UserDTO>() {
@Override
public void onResponse(Call<AuthDTO.UserResponse> call,
Response<AuthDTO.UserResponse> response) {
public void onResponse(Call<UserDTO> call,
Response<UserDTO> response) {
if (response.isSuccessful() && response.body() != null) {
TokenManager.getInstance(MainActivity.this)
.saveUserId(response.body().getId());
@@ -106,7 +107,7 @@ public class MainActivity extends AppCompatActivity {
}
@Override
public void onFailure(Call<AuthDTO.UserResponse> call,
public void onFailure(Call<UserDTO> call,
Throwable t) {
Log.e("MainActivity", "Failed to fetch userId", t);
@@ -129,4 +130,4 @@ public class MainActivity extends AppCompatActivity {
});
});
}
}
}

View File

@@ -1,17 +1,29 @@
package com.example.petstoremobile.api.auth;
import com.example.petstoremobile.dtos.AuthDTO;
import com.example.petstoremobile.dtos.UserDTO;
import java.util.Map;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
//Api for logging in and getting current user
public interface AuthApi {
//login endpoint
@POST("api/v1/auth/login")
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
//get current user endpoint
@GET("api/v1/auth/me")
Call<AuthDTO.UserResponse> getCurrentUser();
Call<UserDTO> getMe();
//update current user endpoint
@PUT("api/v1/auth/me")
Call<UserDTO> updateMe(@Body Map<String, String> updates);
}

View File

@@ -0,0 +1,15 @@
package com.example.petstoremobile.dtos;
//Used to get messages of any errors from the backend
public class ErrorResponse {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,50 @@
package com.example.petstoremobile.dtos;
public class UserDTO {
private Long id;
private String username;
private String email;
private String fullName;
private String phone;
private String avatarUrl;
private String role;
private Long storeId;
private String storeName;
// Getters
public Long getId() {
return id;
}
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public String getFullName() {
return fullName;
}
public String getPhone() {
return phone;
}
public String getAvatarUrl() {
return avatarUrl;
}
public String getRole() {
return role;
}
public Long getStoreId() {
return storeId;
}
public String getStoreName() {
return storeName;
}
}

View File

@@ -16,6 +16,7 @@ import androidx.fragment.app.Fragment;
import android.provider.MediaStore;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -23,20 +24,33 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.example.petstoremobile.R;
import com.example.petstoremobile.activities.MainActivity;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.dtos.ErrorResponse;
import com.example.petstoremobile.dtos.UserDTO;
import com.example.petstoremobile.utils.InputValidator;
import com.google.gson.Gson;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class ProfileFragment extends Fragment {
//initialize the view/controls
private ImageView imgProfile;
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
private Button btnChangePhoto, btnEditEmail, btnEditPhone, btnLogout;
private Uri photoUri;
private UserDTO currentUser;
//Initialize the launchers for camera and gallery
private ActivityResultLauncher<Intent> galleryLauncher;
@@ -107,7 +121,6 @@ public class ProfileFragment extends Fragment {
);
}
//TODO: MAKE PROFILE VIEW DISPLAY PROFILE DATA FROM DATABASE
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -119,10 +132,13 @@ public class ProfileFragment extends Fragment {
tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
tvProfileRole = view.findViewById(R.id.tvProfileRole);
btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
btnEditEmail = view.findViewById(R.id.btnEditEmail);
btnEditPhone = view.findViewById(R.id.btnEditPhone);
btnLogout = view.findViewById(R.id.btnLogout);
Button btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
Button btnEditEmail = view.findViewById(R.id.btnEditEmail);
Button btnEditPhone = view.findViewById(R.id.btnEditPhone);
Button btnLogout = view.findViewById(R.id.btnLogout);
//Load Profile Data from backend
loadProfileData();
//Set up listeners for the buttons
//Change photo button
@@ -170,19 +186,10 @@ public class ProfileFragment extends Fragment {
.setTitle("Edit Email")
.setView(input)
.setPositiveButton("Save", (dialog, which) -> {
String newEmail = input.getText().toString();
//if the new value is a valid email then set the email to the new value
if (android.util.Patterns.EMAIL_ADDRESS.matcher(newEmail).matches()) {
tvProfileEmail.setText(newEmail);
//TODO: UPDATE THE EMAIL IN DATABASE
}
else {
//tell the user to email is invalid
new AlertDialog.Builder(requireContext())
.setTitle("Error")
.setMessage("Email is invalid")
.setPositiveButton("OK", null)
.show();
if (InputValidator.isValidEmail(input)) {
updateProfileField("email", input.getText().toString());
} else {
Toast.makeText(requireContext(), "Email is invalid", Toast.LENGTH_SHORT).show();
}
})
.setNegativeButton("Cancel", null)
@@ -210,19 +217,10 @@ public class ProfileFragment extends Fragment {
.setTitle("Edit Phone Number")
.setView(input)
.setPositiveButton("Save", (dialog, which) -> {
String newPhone = input.getText().toString();
//if the new value is format: (XXX) XXX-XXXX then set the phone to the new value
if (newPhone.matches("\\(\\d{3}\\) \\d{3}-\\d{4}")) { //TODO MAKE VALIDATION CLASS INSTEAD FOR THIS
tvProfilePhone.setText(newPhone);
//TODO: UPDATE PHONE IN DATABASE
}
else {
//tell the user to email cannot be empty
new AlertDialog.Builder(requireContext())
.setTitle("Error")
.setMessage("Phone number is invalid. Format: (XXX) XXX-XXXX")
.setPositiveButton("OK", null)
.show();
if (InputValidator.isValidPhone(input)) {
updateProfileField("phone", input.getText().toString());
} else {
Toast.makeText(requireContext(), "Phone number is invalid", Toast.LENGTH_SHORT).show();
}
})
.setNegativeButton("Cancel", null)
@@ -252,4 +250,72 @@ public class ProfileFragment extends Fragment {
//launch the camera to capture the photo and save the photo to photoUri
cameraLauncher.launch(photoUri);
}
}
//Helper function to call the backend to get profile data and load it to the view
private void loadProfileData() {
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
authApi.getMe().enqueue(new Callback<UserDTO>() {
@Override
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
//if the response is successful and the body is not null then set the user to the view
if (response.isSuccessful() && response.body() != null) {
currentUser = response.body();
//set the user data to the view
tvProfileName.setText(currentUser.getFullName());
tvProfileEmail.setText(currentUser.getEmail());
tvProfilePhone.setText(currentUser.getPhone());
tvProfileRole.setText(currentUser.getRole());
//TODO: LOAD PHOTO FROM DATABASE
}
else {
Log.e("onResponse: ", response.message());
Toast.makeText(getContext(), "Failed to load profile: ", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<UserDTO> call, Throwable t) {
Log.e("PROFILE", "onFailure: " + t.getMessage());
Toast.makeText(getContext(), "Network error: could not load profile", Toast.LENGTH_SHORT).show();
}
});
}
//Helper function to update a profile field in the backend
private void updateProfileField(String fieldName, String value) {
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
Map<String, String> updates = new HashMap<>();
updates.put(fieldName, value);
authApi.updateMe(updates).enqueue(new Callback<UserDTO>() {
@Override
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
if (response.isSuccessful() && response.body() != null) {
currentUser = response.body();
// Update the view with the new data from backend
tvProfileEmail.setText(currentUser.getEmail());
tvProfilePhone.setText(currentUser.getPhone());
Toast.makeText(requireContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
} else {
try {
String errorJson = response.errorBody().string();
ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class);
String errorMessage = errorResponse.getMessage();
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_LONG).show();
} catch (Exception e) {
Log.e("UPDATE_PROFILE", "Error parsing error body", e);
Toast.makeText(requireContext(), "Failed to update profile", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onFailure(Call<UserDTO> call, Throwable t) {
Log.e("UPDATE_PROFILE", "Failure: " + t.getMessage());
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
}
}

View File

@@ -61,10 +61,11 @@ public class InputValidator {
return true;
}
// Checks if the phone number is valid (digits, spaces, dashes, brackets allowed)
// Checks if the phone number is valid
public static boolean isValidPhone(EditText field) {
String phone = field.getText().toString().trim();
if (phone.isEmpty() || !phone.matches("[0-9\\-\\s\\(\\)\\+]+")) {
// Android built in phone validation pattern
if (phone.isEmpty() || !android.util.Patterns.PHONE.matcher(phone).matches()) {
field.setError("Enter a valid phone number");
field.requestFocus();
return false;
@@ -94,4 +95,3 @@ public class InputValidator {
return true;
}
}

View File

@@ -83,7 +83,7 @@
android:id="@+id/tvProfileEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="example@email.com"
android:text="No email loaded"
android:textColor="@color/text_dark"
android:textSize="16sp" />
@@ -129,7 +129,7 @@
android:id="@+id/tvProfilePhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(123) 123-1234"
android:text="No phone loaded"
android:textColor="@color/text_dark"
android:textSize="16sp" />
@@ -174,7 +174,7 @@
android:id="@+id/tvProfileRole"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Manager"
android:text="No role loaded"
android:textSize="16sp"
android:textColor="@color/accent_coral"/>