5
5
namespace YieldStudio \LaravelExpoNotifier ;
6
6
7
7
use Illuminate \Http \Client \PendingRequest ;
8
+ use Illuminate \Http \Client \Response ;
8
9
use Illuminate \Support \Arr ;
9
10
use Illuminate \Support \Collection ;
10
11
use Illuminate \Support \Facades \Http ;
12
+ use YieldStudio \LaravelExpoNotifier \Contracts \ExpoNotificationsServiceInterface ;
11
13
use YieldStudio \LaravelExpoNotifier \Contracts \ExpoPendingNotificationStorageInterface ;
12
14
use YieldStudio \LaravelExpoNotifier \Contracts \ExpoTicketStorageInterface ;
13
15
use YieldStudio \LaravelExpoNotifier \Dto \ExpoMessage ;
17
19
use YieldStudio \LaravelExpoNotifier \Events \InvalidExpoToken ;
18
20
use YieldStudio \LaravelExpoNotifier \Exceptions \ExpoNotificationsException ;
19
21
20
- final class ExpoNotificationsService
22
+ final class ExpoNotificationsService implements ExpoNotificationsServiceInterface
21
23
{
24
+ public const PUSH_NOTIFICATIONS_PER_REQUEST_LIMIT = 100 ;
25
+
26
+ public const SEND_NOTIFICATION_ENDPOINT = '/send ' ;
27
+
22
28
private PendingRequest $ http ;
23
29
30
+ private ?Collection $ expoMessages ;
31
+
32
+ private ?Collection $ notificationsToSend ;
33
+
34
+ private ?Collection $ notificationChunks ;
35
+
36
+ private Collection $ tickets ;
37
+
24
38
public function __construct (
25
39
string $ apiUrl ,
26
40
string $ host ,
@@ -33,62 +47,22 @@ public function __construct(
33
47
'accept-encoding ' => 'gzip, deflate ' ,
34
48
'content-type ' => 'application/json ' ,
35
49
])->baseUrl ($ apiUrl );
50
+
51
+ $ this ->tickets = collect ();
36
52
}
37
53
38
54
/**
39
55
* @param ExpoMessage|ExpoMessage[]|Collection<int, ExpoMessage> $expoMessages
40
56
* @return Collection<int, PushTicketResponse>
41
- *
42
- * @throws ExpoNotificationsException
43
57
*/
44
58
public function notify (ExpoMessage |Collection |array $ expoMessages ): Collection
45
59
{
46
60
/** @var Collection<int, ExpoMessage> $expoMessages */
47
- $ expoMessages = $ expoMessages instanceof Collection ? $ expoMessages : collect (Arr::wrap ($ expoMessages ));
48
-
49
- $ shouldBatchFilter = fn (ExpoMessage $ message ) => $ message ->shouldBatch ;
50
-
51
- // Store notifications to send in the next batch
52
- $ expoMessages
53
- ->filter ($ shouldBatchFilter )
54
- ->each (fn (ExpoMessage $ message ) => $ this ->notificationStorage ->store ($ message ));
55
-
56
- // Filter notifications to send now
57
- $ toSend = $ expoMessages
58
- ->reject ($ shouldBatchFilter )
59
- ->map (fn (ExpoMessage $ message ) => $ message ->toExpoData ())
60
- ->values ();
61
-
62
- if ($ toSend ->isEmpty ()) {
63
- return collect ();
64
- }
65
-
66
- $ response = $ this ->http ->post ('/send ' , $ toSend ->toArray ());
67
- if (! $ response ->successful ()) {
68
- throw new ExpoNotificationsException ($ response ->toPsrResponse ());
69
- }
70
-
71
- $ data = json_decode ($ response ->body (), true );
72
- if (! empty ($ data ['errors ' ])) {
73
- throw new ExpoNotificationsException ($ response ->toPsrResponse ());
74
- }
75
-
76
- $ tickets = collect ($ data ['data ' ])->map (function ($ responseItem ) {
77
- if ($ responseItem ['status ' ] === ExpoResponseStatus::ERROR ->value ) {
78
- return (new PushTicketResponse ())
79
- ->status ($ responseItem ['status ' ])
80
- ->message ($ responseItem ['message ' ])
81
- ->details ($ responseItem ['details ' ]);
82
- }
83
-
84
- return (new PushTicketResponse ())
85
- ->status ($ responseItem ['status ' ])
86
- ->ticketId ($ responseItem ['id ' ]);
87
- });
61
+ $ this ->expoMessages = $ expoMessages instanceof Collection ? $ expoMessages : collect (Arr::wrap ($ expoMessages ));
88
62
89
- $ this ->checkAndStoreTickets ( $ toSend -> pluck ( ' to ' )-> flatten (), $ tickets );
90
-
91
- return $ tickets ;
63
+ return $ this ->storeNotificationsToSendInTheNextBatch ()
64
+ -> prepareNotificationsToSendNow ()
65
+ -> sendNotifications () ;
92
66
}
93
67
94
68
/**
@@ -130,13 +104,17 @@ public function receipts(Collection|array $tokenIds): Collection
130
104
});
131
105
}
132
106
107
+ public function getNotificationChunks (): Collection
108
+ {
109
+ return $ this ->notificationChunks ?? collect ();
110
+ }
111
+
133
112
/**
134
113
* @param Collection<int, string> $tokens
135
- * @param Collection<int, PushTicketResponse> $tickets
136
114
*/
137
- private function checkAndStoreTickets (Collection $ tokens, Collection $ tickets ): void
115
+ private function checkAndStoreTickets (Collection $ tokens ): void
138
116
{
139
- $ tickets
117
+ $ this -> tickets
140
118
->intersectByKeys ($ tokens )
141
119
->each (function (PushTicketResponse $ ticket , $ index ) use ($ tokens ) {
142
120
if ($ ticket ->status === ExpoResponseStatus::ERROR ->value ) {
@@ -152,4 +130,86 @@ private function checkAndStoreTickets(Collection $tokens, Collection $tickets):
152
130
}
153
131
});
154
132
}
133
+
134
+ private function storeNotificationsToSendInTheNextBatch (): ExpoNotificationsService
135
+ {
136
+ $ this ->expoMessages
137
+ ->filter (fn (ExpoMessage $ message ) => $ message ->shouldBatch )
138
+ ->each (fn (ExpoMessage $ message ) => $ this ->notificationStorage ->store ($ message ));
139
+
140
+ return $ this ;
141
+ }
142
+
143
+ private function prepareNotificationsToSendNow (): ExpoNotificationsService
144
+ {
145
+ $ this ->notificationsToSend = $ this ->expoMessages
146
+ ->reject (fn (ExpoMessage $ message ) => $ message ->shouldBatch )
147
+ ->map (fn (ExpoMessage $ message ) => $ message ->toExpoData ())
148
+ ->values ();
149
+
150
+ // Splits into multiples chunks of max limitation
151
+ $ this ->notificationChunks = $ this ->notificationsToSend ->chunk (self ::PUSH_NOTIFICATIONS_PER_REQUEST_LIMIT );
152
+
153
+ return $ this ;
154
+ }
155
+
156
+ private function sendNotifications (): Collection
157
+ {
158
+ if ($ this ->notificationsToSend ->isEmpty ()) {
159
+ return collect ();
160
+ }
161
+
162
+ $ this ->notificationChunks
163
+ ->each (
164
+ fn ($ chunk , $ index ) => $ this ->sendNotificationsChunk ($ chunk ->toArray ())
165
+ );
166
+
167
+ $ this ->checkAndStoreTickets ($ this ->notificationsToSend ->pluck ('to ' )->flatten ());
168
+
169
+ return $ this ->tickets ;
170
+ }
171
+
172
+ private function handleSendNotificationsResponse (Response $ response ): void
173
+ {
174
+ $ data = json_decode ($ response ->body (), true , 512 , JSON_THROW_ON_ERROR );
175
+ if (! empty ($ data ['errors ' ])) {
176
+ throw new ExpoNotificationsException ($ response ->toPsrResponse ());
177
+ }
178
+
179
+ $ this ->setTicketsFromData ($ data );
180
+ }
181
+
182
+ private function setTicketsFromData (array $ data ): ExpoNotificationsService
183
+ {
184
+ collect ($ data ['data ' ])
185
+ ->each (function ($ responseItem ) {
186
+ if ($ responseItem ['status ' ] === ExpoResponseStatus::ERROR ->value ) {
187
+ $ this ->tickets ->push (
188
+ (new PushTicketResponse ())
189
+ ->status ($ responseItem ['status ' ])
190
+ ->message ($ responseItem ['message ' ])
191
+ ->details ($ responseItem ['details ' ])
192
+ );
193
+ } else {
194
+ $ this ->tickets ->push (
195
+ (new PushTicketResponse ())
196
+ ->status ($ responseItem ['status ' ])
197
+ ->ticketId ($ responseItem ['id ' ])
198
+ );
199
+ }
200
+ });
201
+
202
+ return $ this ;
203
+ }
204
+
205
+ private function sendNotificationsChunk (array $ chunk )
206
+ {
207
+ $ response = $ this ->http ->post (self ::SEND_NOTIFICATION_ENDPOINT , $ chunk );
208
+
209
+ if (! $ response ->successful ()) {
210
+ throw new ExpoNotificationsException ($ response ->toPsrResponse ());
211
+ }
212
+
213
+ $ this ->handleSendNotificationsResponse ($ response );
214
+ }
155
215
}
0 commit comments