added push notifications
This commit is contained in:
@@ -52,6 +52,8 @@ nbdist/
|
|||||||
|
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
|
|
||||||
|
frontend/google-services.json
|
||||||
|
|
||||||
frontend/.expo/
|
frontend/.expo/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
frontend/web-build/
|
frontend/web-build/
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ dependencies {
|
|||||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.13.0")
|
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.13.0")
|
||||||
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
|
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
|
||||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0")
|
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0")
|
||||||
|
implementation("org.projectlombok:lombok:1.18.42")
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.42'
|
||||||
implementation 'com.google.firebase:firebase-admin:9.9.0'
|
implementation 'com.google.firebase:firebase-admin:9.9.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS public.subjects
|
|||||||
CONSTRAINT subjects_pkey PRIMARY KEY (id)
|
CONSTRAINT subjects_pkey PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public."user-subject"
|
CREATE TABLE IF NOT EXISTS public."user_subject"
|
||||||
(
|
(
|
||||||
user_id uuid NOT NULL,
|
user_id uuid NOT NULL,
|
||||||
subject_id uuid NOT NULL,
|
subject_id uuid NOT NULL,
|
||||||
|
|||||||
+14
-17
@@ -1,45 +1,42 @@
|
|||||||
package dev.ksan.etfoglasiserver.controller;
|
package dev.ksan.etfoglasiserver.controller;
|
||||||
|
|
||||||
import dev.ksan.etfoglasiserver.model.DeviceToken;
|
import dev.ksan.etfoglasiserver.model.DeviceToken;
|
||||||
|
import dev.ksan.etfoglasiserver.model.RegisterTokenRequest;
|
||||||
import dev.ksan.etfoglasiserver.model.User;
|
import dev.ksan.etfoglasiserver.model.User;
|
||||||
import dev.ksan.etfoglasiserver.repository.DeviceTokenRepo;
|
import dev.ksan.etfoglasiserver.repository.DeviceTokenRepo;
|
||||||
import dev.ksan.etfoglasiserver.repository.UserRepo;
|
import dev.ksan.etfoglasiserver.repository.UserRepo;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
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.PostMapping;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/device-tokens")
|
@RequestMapping("/api/device-tokens")
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class DeviceTokenController {
|
public class DeviceTokenController {
|
||||||
|
|
||||||
@Autowired
|
private final DeviceTokenRepo deviceTokenRepo;
|
||||||
private DeviceTokenRepo deviceTokenRepo;
|
private final UserRepo userRepo;
|
||||||
@Autowired
|
|
||||||
private UserRepo userRepo;
|
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<?> register(
|
public ResponseEntity<?> register(
|
||||||
@RequestParam UUID userId,
|
@RequestBody RegisterTokenRequest request,
|
||||||
@RequestParam String token
|
@AuthenticationPrincipal UserDetails principal) {
|
||||||
) {
|
|
||||||
|
|
||||||
User user = userRepo.findById(userId)
|
User user = userRepo.findByEmail(principal.getUsername())
|
||||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
DeviceToken deviceToken = deviceTokenRepo
|
DeviceToken deviceToken = deviceTokenRepo
|
||||||
.findByFcmToken(token)
|
.findByFcmToken(request.getToken())
|
||||||
.orElse(new DeviceToken());
|
.orElse(new DeviceToken());
|
||||||
|
|
||||||
deviceToken.setUser(user);
|
deviceToken.setUser(user);
|
||||||
deviceToken.setFcmToken(token);
|
deviceToken.setFcmToken(request.getToken());
|
||||||
|
|
||||||
deviceTokenRepo.save(deviceToken);
|
deviceTokenRepo.save(deviceToken);
|
||||||
|
|
||||||
return ResponseEntity.ok("Token saved");
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,11 @@ import dev.ksan.etfoglasiserver.dto.JwtResponseDTO;
|
|||||||
import dev.ksan.etfoglasiserver.dto.UserCreationDTO;
|
import dev.ksan.etfoglasiserver.dto.UserCreationDTO;
|
||||||
import dev.ksan.etfoglasiserver.dto.UserDTO;
|
import dev.ksan.etfoglasiserver.dto.UserDTO;
|
||||||
import dev.ksan.etfoglasiserver.dto.UserLoginDTO;
|
import dev.ksan.etfoglasiserver.dto.UserLoginDTO;
|
||||||
|
import dev.ksan.etfoglasiserver.model.NotificationMethod;
|
||||||
import dev.ksan.etfoglasiserver.model.User;
|
import dev.ksan.etfoglasiserver.model.User;
|
||||||
import dev.ksan.etfoglasiserver.service.UserService;
|
import dev.ksan.etfoglasiserver.service.UserService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -22,13 +25,11 @@ public class UserController {
|
|||||||
|
|
||||||
@GetMapping("users/me")
|
@GetMapping("users/me")
|
||||||
public ResponseEntity<UserDTO> getUserById(Authentication authentication) {
|
public ResponseEntity<UserDTO> getUserById(Authentication authentication) {
|
||||||
|
|
||||||
String email = authentication.getName();
|
String email = authentication.getName();
|
||||||
|
User user = service.getAccountByEmail(email)
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
return ResponseEntity.ok(new UserDTO(user));
|
||||||
User user = service.getAccountByEmail(email).orElseThrow(() -> new RuntimeException("User not found"));
|
|
||||||
|
|
||||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/users/me")
|
@PutMapping("/users/me")
|
||||||
@@ -43,6 +44,20 @@ public class UserController {
|
|||||||
return ResponseEntity.ok(userUpdated);
|
return ResponseEntity.ok(userUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping("users/me/notification-type")
|
||||||
|
public ResponseEntity<?> updateNotificationType(
|
||||||
|
@RequestBody Map<String, String> request,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
String email = authentication.getName();
|
||||||
|
NotificationMethod type = NotificationMethod.valueOf(request.get("notificationType"));
|
||||||
|
service.updateNotificationType(email, type);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("status", "updated"));
|
||||||
|
}
|
||||||
|
|
||||||
@PutMapping("/users/me/subjects/{subjectId}")
|
@PutMapping("/users/me/subjects/{subjectId}")
|
||||||
public ResponseEntity<UserDTO> addSubject(
|
public ResponseEntity<UserDTO> addSubject(
|
||||||
@PathVariable UUID subjectId,
|
@PathVariable UUID subjectId,
|
||||||
@@ -83,6 +98,8 @@ public class UserController {
|
|||||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@DeleteMapping("/users/me")
|
@DeleteMapping("/users/me")
|
||||||
public ResponseEntity<Void> deleteUser(Authentication authentication) {
|
public ResponseEntity<Void> deleteUser(Authentication authentication) {
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,25 @@ package dev.ksan.etfoglasiserver.dto;
|
|||||||
import dev.ksan.etfoglasiserver.model.NotificationMethod;
|
import dev.ksan.etfoglasiserver.model.NotificationMethod;
|
||||||
import dev.ksan.etfoglasiserver.model.Subject;
|
import dev.ksan.etfoglasiserver.model.Subject;
|
||||||
import dev.ksan.etfoglasiserver.model.User;
|
import dev.ksan.etfoglasiserver.model.User;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class UserDTO {
|
public class UserDTO {
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
private UUID id;
|
private UUID id;
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
private String email;
|
private String email;
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
private Set<Subject> subjectSet;
|
private Set<Subject> subjectSet;
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private NotificationMethod notificationType;
|
||||||
|
|
||||||
public UserDTO() {}
|
public UserDTO() {}
|
||||||
|
|
||||||
@@ -19,38 +30,17 @@ public class UserDTO {
|
|||||||
this.id = id;
|
this.id = id;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.subjectSet = subjectSet;
|
this.subjectSet = subjectSet;
|
||||||
|
this.notificationType = notificationMethod2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserDTO(User user) {
|
public UserDTO(User user) {
|
||||||
this.id = user.getId();
|
this.id = user.getId();
|
||||||
this.email = user.getEmail();
|
this.email = user.getEmail();
|
||||||
this.subjectSet = user.getSubjectSet();
|
this.subjectSet = user.getSubjectSet();
|
||||||
|
this.notificationType = user.getNotificationMethod();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(UUID id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getEmail() {
|
|
||||||
return email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEmail(String email) {
|
|
||||||
this.email = email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Subject> getSubjectSet() {
|
|
||||||
return subjectSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubjectSet(Set<Subject> subjectSet) {
|
|
||||||
this.subjectSet = subjectSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import lombok.Setter;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Getter // ← add class-level getter
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "device_tokens",
|
name = "device_tokens",
|
||||||
@@ -25,7 +26,6 @@ public class DeviceToken {
|
|||||||
@JoinColumn(name = "user_id", nullable = false)
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
private User user;
|
private User user;
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
@Setter
|
||||||
@Column(name = "fcm_token", nullable = false)
|
@Column(name = "fcm_token", nullable = false)
|
||||||
private String fcmToken;
|
private String fcmToken;
|
||||||
@@ -38,5 +38,4 @@ public class DeviceToken {
|
|||||||
public void touch() {
|
public void touch() {
|
||||||
this.updatedAt = LocalDateTime.now();
|
this.updatedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,5 @@ package dev.ksan.etfoglasiserver.model;
|
|||||||
public enum NotificationMethod {
|
public enum NotificationMethod {
|
||||||
|
|
||||||
NO_NOTIFICATION,
|
NO_NOTIFICATION,
|
||||||
EMAIL,
|
|
||||||
PUSH_NOTIFICATION
|
PUSH_NOTIFICATION
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.ksan.etfoglasiserver.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RegisterTokenRequest {
|
||||||
|
private String token;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package dev.ksan.etfoglasiserver.model;
|
package dev.ksan.etfoglasiserver.model;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -9,13 +11,18 @@ import java.util.UUID;
|
|||||||
@Table(name = "subjects")
|
@Table(name = "subjects")
|
||||||
public class Subject {
|
public class Subject {
|
||||||
|
|
||||||
|
@Getter
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
@Column(columnDefinition = "uuid")
|
@Column(columnDefinition = "uuid")
|
||||||
private UUID id;
|
private UUID id;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
@Column private long code;
|
@Column private long code;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@@ -26,23 +33,4 @@ public class Subject {
|
|||||||
public Subject(){
|
public Subject(){
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCode(long code) {
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import jakarta.persistence.Id;
|
|||||||
import jakarta.persistence.JoinColumn;
|
import jakarta.persistence.JoinColumn;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -18,9 +20,11 @@ public class SubjectAlt {
|
|||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
private UUID id;
|
private UUID id;
|
||||||
|
|
||||||
|
@Setter
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
String name;
|
String name;
|
||||||
|
|
||||||
|
@Setter
|
||||||
@ManyToOne(fetch = FetchType.EAGER)
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
@JoinColumn(nullable = false)
|
@JoinColumn(nullable = false)
|
||||||
private Subject subject;
|
private Subject subject;
|
||||||
@@ -32,22 +36,15 @@ public class SubjectAlt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public UUID getId() {
|
public UUID getId() {
|
||||||
return id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return this.name;
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Subject getSubject() {
|
public Subject getSubject() {
|
||||||
return subject;
|
return this.subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSubject(Subject subject) {
|
|
||||||
this.subject = subject;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ public class EntryService {
|
|||||||
Entry newEntry = new Entry(entry);
|
Entry newEntry = new Entry(entry);
|
||||||
newEntry.setSubject(subject);
|
newEntry.setSubject(subject);
|
||||||
entryRepo.save(newEntry);
|
entryRepo.save(newEntry);
|
||||||
|
|
||||||
|
subjectService.notifyAsync(newEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addEntry(Entry entry) {
|
public void addEntry(Entry entry) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import dev.ksan.etfoglasiserver.dto.JwtResponseDTO;
|
|||||||
import dev.ksan.etfoglasiserver.dto.UserCreationDTO;
|
import dev.ksan.etfoglasiserver.dto.UserCreationDTO;
|
||||||
import dev.ksan.etfoglasiserver.dto.UserDTO;
|
import dev.ksan.etfoglasiserver.dto.UserDTO;
|
||||||
import dev.ksan.etfoglasiserver.dto.UserLoginDTO;
|
import dev.ksan.etfoglasiserver.dto.UserLoginDTO;
|
||||||
|
import dev.ksan.etfoglasiserver.model.NotificationMethod;
|
||||||
import dev.ksan.etfoglasiserver.model.Subject;
|
import dev.ksan.etfoglasiserver.model.Subject;
|
||||||
import dev.ksan.etfoglasiserver.model.User;
|
import dev.ksan.etfoglasiserver.model.User;
|
||||||
import dev.ksan.etfoglasiserver.repository.SubjectRepo;
|
import dev.ksan.etfoglasiserver.repository.SubjectRepo;
|
||||||
@@ -204,4 +205,11 @@ public class UserService {
|
|||||||
|
|
||||||
return getUserDTO(user);
|
return getUserDTO(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateNotificationType(String email, NotificationMethod notificationType) {
|
||||||
|
User user = userRepo.findByEmail(email).orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
user.setNotificationMethod(notificationType);
|
||||||
|
userRepo.save(user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-6
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "etfoglasi-frontend",
|
"name": "etfoglasi",
|
||||||
"slug": "etfoglasi-frontend",
|
"slug": "etfoglasi",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "etfoglasifrontend",
|
"scheme": "etfoglasi",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true
|
"supportsTablet": true
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
"googleServicesFile": "./google-services.json",
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#E6F4FE",
|
"backgroundColor": "#E6F4FE",
|
||||||
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
||||||
@@ -19,7 +20,8 @@
|
|||||||
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"predictiveBackGestureEnabled": false
|
"predictiveBackGestureEnabled": false,
|
||||||
|
"package": "dev.ksan.etfoglasi"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"output": "static",
|
"output": "static",
|
||||||
@@ -39,7 +41,9 @@
|
|||||||
"backgroundColor": "#000000"
|
"backgroundColor": "#000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"@react-native-firebase/app",
|
||||||
|
"@react-native-firebase/messaging"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true,
|
"typedRoutes": true,
|
||||||
@@ -47,4 +51,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,62 @@
|
|||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
import { Stack } from "expo-router";
|
import { useEffect } from 'react';
|
||||||
import {AuthProvider} from "@/context/AuthContext";
|
import { Stack } from 'expo-router';
|
||||||
import {SafeAreaProvider} from "react-native-safe-area-context";
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
|
import {
|
||||||
|
getMessaging,
|
||||||
|
onMessage,
|
||||||
|
onNotificationOpenedApp,
|
||||||
|
getInitialNotification,
|
||||||
|
} from '@react-native-firebase/messaging';
|
||||||
|
import { registerDeviceToken, listenForTokenRefresh } from '@/services/notifications';
|
||||||
|
import { AuthProvider, useAuth } from "@/context/AuthContext";
|
||||||
|
import Toast from 'react-native-toast-message';
|
||||||
|
|
||||||
|
function NotificationSetup() {
|
||||||
|
const { user, loading } = useAuth();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading || !user) return;
|
||||||
|
|
||||||
|
registerDeviceToken(user.token);
|
||||||
|
|
||||||
|
const messaging = getMessaging();
|
||||||
|
|
||||||
|
const unsubRefresh = listenForTokenRefresh(user.token);
|
||||||
|
const unsubForeground = onMessage(messaging, async (msg) => {
|
||||||
|
console.log('Foreground notification:', msg);
|
||||||
|
Toast.show({
|
||||||
|
type: 'info',
|
||||||
|
text1: msg.notification?.title ?? 'New Notification',
|
||||||
|
position: 'top',
|
||||||
|
visibilityTime: 4000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onNotificationOpenedApp(messaging, (msg) => {
|
||||||
|
console.log('Opened from background:', msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
getInitialNotification(messaging).then((msg) => {
|
||||||
|
if (msg) console.log('Opened from quit state:', msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubRefresh();
|
||||||
|
unsubForeground();
|
||||||
|
};
|
||||||
|
}, [user, loading]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
|
<NotificationSetup />
|
||||||
<Stack screenOptions={{ headerShown: false }} />
|
<Stack screenOptions={{ headerShown: false }} />
|
||||||
|
<Toast />
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
|
|||||||
import {
|
import {
|
||||||
authApi,
|
authApi,
|
||||||
|
|
||||||
LoginPayload, RegisterPayload, subscriptionsApi, UserUpdateDTO,
|
LoginPayload, RegisterPayload, subscriptionsApi, userApi, UserUpdateDTO,
|
||||||
} from "@/services/api";
|
} from "@/services/api";
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
@@ -11,6 +11,7 @@ type User = {
|
|||||||
email: string;
|
email: string;
|
||||||
token: string;
|
token: string;
|
||||||
subscribedSubjectIds: string[];
|
subscribedSubjectIds: string[];
|
||||||
|
notificationType: 'PUSH_NOTIFICATION' | 'NO_NOTIFICATION';
|
||||||
};
|
};
|
||||||
|
|
||||||
type AuthContextValue = {
|
type AuthContextValue = {
|
||||||
@@ -23,6 +24,7 @@ type AuthContextValue = {
|
|||||||
unsubscribe: (subjectId: string) => Promise<void>;
|
unsubscribe: (subjectId: string) => Promise<void>;
|
||||||
isSubscribed: (subjectId: string) => boolean;
|
isSubscribed: (subjectId: string) => boolean;
|
||||||
updateUser: (newEmail?: string, newPassword?: string) => Promise<void>;
|
updateUser: (newEmail?: string, newPassword?: string) => Promise<void>;
|
||||||
|
updateNotificationType: (type: 'PUSH_NOTIFICATION' | 'NO_NOTIFICATION') => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||||
@@ -43,17 +45,24 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setUser(u);
|
setUser(u);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateNotificationType = async (type: 'PUSH_NOTIFICATION' | 'NO_NOTIFICATION') => {
|
||||||
|
if (!user) return;
|
||||||
|
await userApi.updateNotificationType(type, user.token);
|
||||||
|
await persist({ ...user, notificationType: type });
|
||||||
|
};
|
||||||
|
|
||||||
const login = async (payload: LoginPayload) => {
|
const login = async (payload: LoginPayload) => {
|
||||||
const res = await authApi.login(payload);
|
const res = await authApi.login(payload);
|
||||||
|
|
||||||
// Gracefully fetch subscriptions — don't block login if this fails
|
|
||||||
let subscribedSubjectIds: string[] = [];
|
let subscribedSubjectIds: string[] = [];
|
||||||
|
let notificationType: 'PUSH_NOTIFICATION' | 'NO_NOTIFICATION' = 'PUSH_NOTIFICATION';
|
||||||
|
|
||||||
if (res.userId) {
|
if (res.userId) {
|
||||||
try {
|
try {
|
||||||
const dto = await authApi.getUser( res.token);
|
const dto = await authApi.getUser(res.token);
|
||||||
subscribedSubjectIds = dto.subjectSet?.map((s) => s.id) ?? [];
|
subscribedSubjectIds = dto.subjectSet?.map((s) => s.id) ?? [];
|
||||||
} catch {
|
notificationType = dto.notificationType ?? 'PUSH_NOTIFICATION';
|
||||||
// aaaa
|
} catch (e) {
|
||||||
|
console.error('Failed to load subscriptions on login:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +71,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
email: res.email,
|
email: res.email,
|
||||||
token: res.token,
|
token: res.token,
|
||||||
subscribedSubjectIds,
|
subscribedSubjectIds,
|
||||||
|
notificationType,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,7 +126,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
user?.subscribedSubjectIds.includes(subjectId) ?? false;
|
user?.subscribedSubjectIds.includes(subjectId) ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, loading, login, register, logout, subscribe, unsubscribe, isSubscribed, updateUser }}>
|
<AuthContext.Provider value={{ user, loading, login, register, logout, subscribe, unsubscribe, isSubscribed, updateUser, updateNotificationType }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-2
@@ -15,8 +15,9 @@ import {enableScreens} from "react-native-screens";
|
|||||||
import {SafeAreaProvider} from "react-native-safe-area-context";
|
import {SafeAreaProvider} from "react-native-safe-area-context";
|
||||||
import AuthGate from "@/screens/AuthGate";
|
import AuthGate from "@/screens/AuthGate";
|
||||||
import {AuthProvider, useAuth} from "@/context/AuthContext";
|
import {AuthProvider, useAuth} from "@/context/AuthContext";
|
||||||
|
import messaging, {setBackgroundMessageHandler} from "@react-native-firebase/messaging";
|
||||||
|
import {getMessaging} from "@firebase/messaging";
|
||||||
|
messaging().setBackgroundMessageHandler(async () => {});
|
||||||
// Must be called before any navigator renders
|
// Must be called before any navigator renders
|
||||||
enableScreens();
|
enableScreens();
|
||||||
|
|
||||||
|
|||||||
Generated
+1325
-432
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "etfoglasi-frontend",
|
"name": "etfoglasi",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
"android": "expo start --android",
|
"android": "expo run:android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
|
"@react-native-firebase/app": "^24.0.0",
|
||||||
|
"@react-native-firebase/messaging": "^24.0.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-tailwindcss": "^1.1.11",
|
"react-native-tailwindcss": "^1.1.11",
|
||||||
|
"react-native-toast-message": "^2.3.3",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.5.1",
|
"react-native-worklets": "0.5.1",
|
||||||
"tailwindcss": "^3.4.18"
|
"tailwindcss": "^3.4.18"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, {useCallback, useState} from "react";
|
||||||
import {
|
import {
|
||||||
View, Text, ScrollView, TouchableOpacity,
|
View, Text, ScrollView, TouchableOpacity,
|
||||||
ActivityIndicator, useColorScheme,
|
ActivityIndicator, useColorScheme,
|
||||||
@@ -7,6 +7,7 @@ import { SafeAreaView } from "react-native-safe-area-context";
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import SearchBar from "../components/SearchBar";
|
import SearchBar from "../components/SearchBar";
|
||||||
import CollapsibleCategory from "../components/CollapsibleCategory";
|
import CollapsibleCategory from "../components/CollapsibleCategory";
|
||||||
|
import {useFocusEffect} from "expo-router";
|
||||||
|
|
||||||
|
|
||||||
const GROUPS = [
|
const GROUPS = [
|
||||||
@@ -40,6 +41,12 @@ export default function AllFeed() {
|
|||||||
setTimeout(() => setRefreshing(false), 800);
|
setTimeout(() => setRefreshing(false), 800);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
handleRefresh();
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className={`flex-1 ${dark ? "bg-obsidian-200" : "bg-parchment-50"}`}>
|
<SafeAreaView className={`flex-1 ${dark ? "bg-obsidian-200" : "bg-parchment-50"}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ type SettingRowProps = {
|
|||||||
accent?: string;
|
accent?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function SettingRow({ icon, label, value, toggle, toggleValue, onToggle, onPress, accent }: SettingRowProps) {
|
function SettingRow({ icon, label, value, toggle, toggleValue, onToggle, onPress, accent }: SettingRowProps) {
|
||||||
const dark = useColorScheme() === "dark";
|
const dark = useColorScheme() === "dark";
|
||||||
const iconColor = accent ?? (dark ? "#706D67" : "#8A8278");
|
const iconColor = accent ?? (dark ? "#706D67" : "#8A8278");
|
||||||
@@ -149,11 +151,25 @@ function GuestProfile({ onSignIn }: { onSignIn: () => void }) {
|
|||||||
function AuthenticatedProfile({ onSignOut }: { onSignOut: () => void }) {
|
function AuthenticatedProfile({ onSignOut }: { onSignOut: () => void }) {
|
||||||
const [showSubs, setShowSubs] = useState(false);
|
const [showSubs, setShowSubs] = useState(false);
|
||||||
const dark = useColorScheme() === "dark";
|
const dark = useColorScheme() === "dark";
|
||||||
const { user } = useAuth();
|
const { user, updateNotificationType } = useAuth();
|
||||||
const [notifications, setNotifications] = React.useState(true);
|
const [notifications, setNotifications] = useState(
|
||||||
|
user?.notificationType === 'PUSH_NOTIFICATION'
|
||||||
|
);
|
||||||
const [digest, setDigest] = React.useState(false);
|
const [digest, setDigest] = React.useState(false);
|
||||||
const [showEdit, setShowEdit] = useState(false);
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
|
|
||||||
|
const handleNotificationToggle = async (value: boolean) => {
|
||||||
|
setNotifications(value);
|
||||||
|
try {
|
||||||
|
await updateNotificationType(
|
||||||
|
value ? 'PUSH_NOTIFICATION' : 'NO_NOTIFICATION'
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
setNotifications(!value); // revert on failure
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrollView contentContainerStyle={{ paddingBottom: 48 }} showsVerticalScrollIndicator={false}>
|
<ScrollView contentContainerStyle={{ paddingBottom: 48 }} showsVerticalScrollIndicator={false}>
|
||||||
@@ -197,13 +213,19 @@ function AuthenticatedProfile({ onSignOut }: { onSignOut: () => void }) {
|
|||||||
>
|
>
|
||||||
<SectionLabel title="Notifications" />
|
<SectionLabel title="Notifications" />
|
||||||
<SettingRow
|
<SettingRow
|
||||||
icon="notifications-outline" label="Push notifications (TODO)"
|
icon="notifications-outline"
|
||||||
toggle toggleValue={notifications} onToggle={setNotifications}
|
label="Push notifications"
|
||||||
|
toggle
|
||||||
|
toggleValue={notifications}
|
||||||
|
onToggle={handleNotificationToggle}
|
||||||
accent={dark ? "#E07B45" : "#C4622D"}
|
accent={dark ? "#E07B45" : "#C4622D"}
|
||||||
/>
|
/>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
icon="mail-outline" label="Daily digest email (TODO)"
|
icon="mail-outline"
|
||||||
toggle toggleValue={digest} onToggle={setDigest}
|
label="Weekly Digest email (TODO)"
|
||||||
|
toggle
|
||||||
|
toggleValue={digest}
|
||||||
|
onToggle={setDigest}
|
||||||
accent={dark ? "#5E9EF4" : "#3B7DD8"}
|
accent={dark ? "#5E9EF4" : "#3B7DD8"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -231,32 +253,21 @@ function AuthenticatedProfile({ onSignOut }: { onSignOut: () => void }) {
|
|||||||
<Text className="text-sm font-sans font-semibold text-red-500">Sign out</Text>
|
<Text className="text-sm font-sans font-semibold text-red-500">Sign out</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<Modal visible={showEdit} animationType="slide" presentationStyle="pageSheet" onRequestClose={() => setShowEdit(false)}>
|
<Modal visible={showEdit} animationType="slide" presentationStyle="pageSheet" onRequestClose={() => setShowEdit(false)}>
|
||||||
<EditProfile onClose={() => setShowEdit(false)} />
|
<EditProfile onClose={() => setShowEdit(false)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal visible={showHelp} animationType="slide" presentationStyle="pageSheet" onRequestClose={() => setShowHelp(false)}>
|
<Modal visible={showHelp} animationType="slide" presentationStyle="pageSheet" onRequestClose={() => setShowHelp(false)}>
|
||||||
<HelpSupport onClose={() => setShowHelp(false)} />
|
<HelpSupport onClose={() => setShowHelp(false)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal visible={showSubs} animationType="slide" presentationStyle="pageSheet" onRequestClose={() => setShowSubs(false)}>
|
||||||
visible={showSubs}
|
|
||||||
animationType="slide"
|
|
||||||
presentationStyle="pageSheet"
|
|
||||||
onRequestClose={() => setShowSubs(false)}
|
|
||||||
>
|
|
||||||
<ManageSubscriptions onClose={() => setShowSubs(false)} />
|
<ManageSubscriptions onClose={() => setShowSubs(false)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const dark = useColorScheme() === "dark";
|
const dark = useColorScheme() === "dark";
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SearchBar from "../components/SearchBar";
|
|||||||
import ExpandableItem from "../components/ExpandableItem";
|
import ExpandableItem from "../components/ExpandableItem";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Entry, entriesApi } from "../services/api";
|
import { Entry, entriesApi } from "../services/api";
|
||||||
|
import {useFocusEffect} from "expo-router";
|
||||||
|
|
||||||
type SortMode = "time" | "subject";
|
type SortMode = "time" | "subject";
|
||||||
|
|
||||||
@@ -124,9 +125,14 @@ export default function SubscribedFeed() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [user?.subscribedSubjectIds]);
|
}, [user?.subscribedSubjectIds?.join(',')]);
|
||||||
|
|
||||||
useEffect(() => { fetchFeed(); }, [fetchFeed]);
|
// Always re-fetch when tab gains focus
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
fetchFeed();
|
||||||
|
}, [fetchFeed])
|
||||||
|
);
|
||||||
|
|
||||||
// Filter then group
|
// Filter then group
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
@@ -249,7 +255,7 @@ export default function SubscribedFeed() {
|
|||||||
<View className="items-center mt-16 px-8">
|
<View className="items-center mt-16 px-8">
|
||||||
<Text className="text-4xl mb-3">🔍</Text>
|
<Text className="text-4xl mb-3">🔍</Text>
|
||||||
<Text className={`text-base font-sans font-semibold text-center ${dark ? "text-ink-dark" : "text-ink-light"}`}>
|
<Text className={`text-base font-sans font-semibold text-center ${dark ? "text-ink-dark" : "text-ink-light"}`}>
|
||||||
No results for "{query}"
|
No results for "{query}"
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
const BASE_URL = process.env.EXPO_PUBLIC_API_URL; // TODO change this
|
const BASE_URL = process.env.EXPO_PUBLIC_API_URL; // TODO change this
|
||||||
|
|
||||||
|
export type NotificationType = 'PUSH_NOTIFICATION' | 'NO_NOTIFICATION';
|
||||||
function authHeader(token?: string) {
|
function authHeader(token?: string) {
|
||||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,13 @@ async function request<T>(
|
|||||||
headers: { "Content-Type": "application/json", ...authHeader(token) },
|
headers: { "Content-Type": "application/json", ...authHeader(token) },
|
||||||
...(body ? { body: JSON.stringify(body) } : {}),
|
...(body ? { body: JSON.stringify(body) } : {}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(await res.text().catch(() => `HTTP ${res.status}`));
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text().catch(() => `HTTP ${res.status}`);
|
||||||
|
console.log('Failed URL:', res.url);
|
||||||
|
console.log('Status:', res.status);
|
||||||
|
console.log('Response:', errorText);
|
||||||
|
throw new Error(errorText);
|
||||||
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +71,7 @@ export type UserDTO = {
|
|||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
subjectSet: Subject[];
|
subjectSet: Subject[];
|
||||||
|
notificationType: NotificationType;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,6 +122,12 @@ export const authApi = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
updateNotificationType: (
|
||||||
|
type: 'PUSH_NOTIFICATION' | 'NO_NOTIFICATION',
|
||||||
|
token: string
|
||||||
|
) => post<void>('/users/me/notification-type', { notificationType: type }, token),
|
||||||
|
};
|
||||||
|
|
||||||
export const subjectsApi = {
|
export const subjectsApi = {
|
||||||
getAll: (token?: string) => get<Subject[]>("/subjects", token),
|
getAll: (token?: string) => get<Subject[]>("/subjects", token),
|
||||||
@@ -135,6 +148,8 @@ export const subscriptionsApi = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const entriesApi = {
|
export const entriesApi = {
|
||||||
getEntries: (params: { subjectId?: string; groupName?: string; page?: number }) =>
|
getEntries: (params: { subjectId?: string; groupName?: string; page?: number }) =>
|
||||||
get<SpringPage<Entry>>("/entries", undefined, params),
|
get<SpringPage<Entry>>("/entries", undefined, params),
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
getMessaging,
|
||||||
|
requestPermission,
|
||||||
|
getToken,
|
||||||
|
onTokenRefresh,
|
||||||
|
AuthorizationStatus,
|
||||||
|
} from '@react-native-firebase/messaging';
|
||||||
|
import { post } from '@/services/api';
|
||||||
|
|
||||||
|
export async function registerDeviceToken(authToken: string): Promise<void> {
|
||||||
|
const messaging = getMessaging();
|
||||||
|
const status = await requestPermission(messaging);
|
||||||
|
const granted =
|
||||||
|
status === AuthorizationStatus.AUTHORIZED ||
|
||||||
|
status === AuthorizationStatus.PROVISIONAL;
|
||||||
|
|
||||||
|
if (!granted) return;
|
||||||
|
|
||||||
|
const fcmToken = await getToken(messaging);
|
||||||
|
|
||||||
|
await fetch(`${process.env.EXPO_PUBLIC_API_URL}/device-tokens/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${authToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ token: fcmToken }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenForTokenRefresh(authToken: string): () => void {
|
||||||
|
return onTokenRefresh(getMessaging(), async (newToken) => {
|
||||||
|
await post('/device-tokens/register', { token: newToken }, authToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user