apns_h2/request/notification/default.rs
1use crate::InterruptionLevel;
2use crate::request::notification::{NotificationBuilder, NotificationOptions};
3use crate::request::payload::{APS, APSAlert, APSSound, Payload};
4
5use std::{borrow::Cow, collections::BTreeMap};
6
7/// Represents a bool that serializes as a u8 0/1 for false/true respectively
8mod bool_as_u8 {
9 use serde::{
10 Deserialize,
11 de::{self, Deserializer, Unexpected},
12 ser::Serializer,
13 };
14
15 pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
16 where
17 D: Deserializer<'de>,
18 {
19 match u8::deserialize(deserializer)? {
20 0 => Ok(false),
21 1 => Ok(true),
22 other => Err(de::Error::invalid_value(
23 Unexpected::Unsigned(other as u64),
24 &"zero or one",
25 )),
26 }
27 }
28
29 pub fn serialize<S>(value: &bool, serializer: S) -> Result<S::Ok, S::Error>
30 where
31 S: Serializer,
32 {
33 serializer.serialize_u8(match value {
34 false => 0,
35 true => 1,
36 })
37 }
38}
39
40#[derive(Deserialize, Serialize, Debug, Clone, Default)]
41#[serde(rename_all = "kebab-case")]
42pub struct DefaultSound<'a> {
43 #[serde(skip_serializing_if = "std::ops::Not::not", with = "bool_as_u8")]
44 critical: bool,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
47 name: Option<Cow<'a, str>>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
50 volume: Option<f64>,
51}
52
53#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq)]
54#[serde(rename_all = "kebab-case")]
55pub struct DefaultAlert<'a> {
56 #[serde(skip_serializing_if = "Option::is_none")]
57 title: Option<Cow<'a, str>>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
60 subtitle: Option<Cow<'a, str>>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
63 body: Option<Cow<'a, str>>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
66 launch_image: Option<Cow<'a, str>>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
69 title_loc_key: Option<Cow<'a, str>>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
72 title_loc_args: Option<Vec<Cow<'a, str>>>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
75 subtitle_loc_key: Option<Cow<'a, str>>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
78 subtitle_loc_args: Option<Vec<Cow<'a, str>>>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
81 action_loc_key: Option<Cow<'a, str>>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
84 loc_key: Option<Cow<'a, str>>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
87 loc_args: Option<Vec<Cow<'a, str>>>,
88}
89
90impl<'a> DefaultAlert<'a> {
91 pub(crate) fn body_only(body: Cow<'a, str>) -> Self {
92 DefaultAlert::<'_> {
93 body: Some(body),
94 ..Default::default()
95 }
96 }
97}
98
99/// A builder to create an APNs payload.
100///
101/// # Example
102///
103/// ```rust
104/// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
105/// # use apns_h2::request::payload::PayloadLike;
106/// # fn main() {
107/// let mut builder = DefaultNotificationBuilder::new()
108/// .title("Hi there")
109/// .subtitle("From bob")
110/// .body("What's up?")
111/// .badge(420)
112/// .category("cat1")
113/// .sound("prööt")
114/// .thread_id("my-thread")
115/// .critical(false, None)
116/// .mutable_content()
117/// .action_loc_key("PLAY")
118/// .launch_image("foo.jpg")
119/// .loc_args(&["argh", "narf"])
120/// .title_loc_key("STOP")
121/// .title_loc_args(&["herp", "derp"])
122/// .loc_key("PAUSE")
123/// .loc_args(&["narf", "derp"]);
124/// let payload = builder.build("device_id", Default::default())
125/// .to_json_string().unwrap();
126/// # }
127/// ```
128#[derive(Debug, Clone, Default)]
129pub struct DefaultNotificationBuilder<'a> {
130 alert: DefaultAlert<'a>,
131 badge: Option<u32>,
132 sound: DefaultSound<'a>,
133 thread_id: Option<Cow<'a, str>>,
134 category: Option<Cow<'a, str>>,
135 mutable_content: u8,
136 content_available: Option<u8>,
137 interruption_level: Option<InterruptionLevel>,
138 timestamp: Option<u64>,
139 event: Option<Cow<'a, str>>,
140 content_state: Option<serde_json::Value>,
141 attributes_type: Option<Cow<'a, str>>,
142 attributes: Option<serde_json::Value>,
143 input_push_channel: Option<Cow<'a, str>>,
144 input_push_token: Option<u8>,
145 dismissal_date: Option<u64>,
146}
147
148impl<'a> DefaultNotificationBuilder<'a> {
149 /// Creates a new builder with the minimum amount of content.
150 ///
151 /// ```rust
152 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
153 /// # use apns_h2::request::payload::PayloadLike;
154 /// # fn main() {
155 /// let payload = DefaultNotificationBuilder::new()
156 /// .title("a title")
157 /// .body("a body")
158 /// .build("token", Default::default());
159 ///
160 /// assert_eq!(
161 /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"body\":\"a body\"},\"mutable-content\":0}}",
162 /// &payload.to_json_string().unwrap()
163 /// );
164 /// # }
165 /// ```
166 pub fn new() -> DefaultNotificationBuilder<'a> {
167 Self::default()
168 }
169
170 /// Set the title of the notification.
171 /// Apple Watch displays this string in the short look notification interface.
172 /// Specify a string that's quickly understood by the user.
173 ///
174 /// ```rust
175 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
176 /// # use apns_h2::request::payload::PayloadLike;
177 /// # fn main() {
178 /// let mut builder = DefaultNotificationBuilder::new()
179 /// .title("a title");
180 /// let payload = builder.build("token", Default::default());
181 ///
182 /// assert_eq!(
183 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0}}",
184 /// &payload.to_json_string().unwrap()
185 /// );
186 /// # }
187 /// ```
188 pub fn title(mut self, title: impl Into<Cow<'a, str>>) -> Self {
189 self.alert.title = Some(title.into());
190 self
191 }
192
193 #[deprecated(
194 since = "0.11.0",
195 note = "Use the idiomatic `title` instead of the legacy `set_*` fn"
196 )]
197 pub fn set_title(self, title: impl Into<Cow<'a, str>>) -> Self {
198 self.title(title)
199 }
200
201 /// Set critical alert value for this notification
202 /// Volume can only be set when the notification is marked as critcial
203 /// Note: You'll need the [critical alerts entitlement](https://developer.apple.com/contact/request/notifications-critical-alerts-entitlement/) to use `true`!
204 ///
205 /// ```rust
206 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
207 /// # use apns_h2::request::payload::PayloadLike;
208 /// # fn main() {
209 /// let mut builder = DefaultNotificationBuilder::new()
210 /// .critical(true, None);
211 /// let payload = builder.build("token", Default::default());
212 ///
213 /// assert_eq!(
214 /// "{\"aps\":{\"sound\":{\"critical\":1},\"mutable-content\":0}}",
215 /// &payload.to_json_string().unwrap()
216 /// );
217 /// # }
218 /// ```
219 pub fn critical(mut self, critical: bool, volume: Option<f64>) -> Self {
220 if !critical {
221 self.sound.volume = None;
222 self.sound.critical = false;
223 } else {
224 self.sound.volume = volume;
225 self.sound.critical = true;
226 }
227 self
228 }
229
230 #[deprecated(
231 since = "0.11.0",
232 note = "Use the idiomatic `critical` instead of the legacy `set_*` fn"
233 )]
234 pub fn set_critical(self, critical: bool, volume: Option<f64>) -> Self {
235 self.critical(critical, volume)
236 }
237
238 /// Used to set the subtitle which should provide additional information that explains the purpose of the notification.
239 ///
240 /// ```rust
241 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
242 /// # use apns_h2::request::payload::PayloadLike;
243 /// # fn main() {
244 /// let mut builder = DefaultNotificationBuilder::new()
245 /// .subtitle("a subtitle");
246 /// let payload = builder.build("token", Default::default());
247 ///
248 /// assert_eq!(
249 /// "{\"aps\":{\"alert\":{\"subtitle\":\"a subtitle\"},\"mutable-content\":0}}",
250 /// &payload.to_json_string().unwrap()
251 /// );
252 /// # }
253 /// ```
254 pub fn subtitle(mut self, subtitle: impl Into<Cow<'a, str>>) -> Self {
255 self.alert.subtitle = Some(subtitle.into());
256 self
257 }
258
259 #[deprecated(
260 since = "0.11.0",
261 note = "Use the idiomatic `subtitle` instead of the legacy `set_*` fn"
262 )]
263 pub fn set_subtitle(self, subtitle: impl Into<Cow<'a, str>>) -> Self {
264 self.subtitle(subtitle)
265 }
266
267 /// Sets the content of the alert message.
268 ///
269 /// ```rust
270 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
271 /// # use apns_h2::request::payload::PayloadLike;
272 /// # fn main() {
273 /// let mut builder = DefaultNotificationBuilder::new()
274 /// .body("a body");
275 /// let payload = builder.build("token", Default::default());
276 ///
277 /// assert_eq!(
278 /// "{\"aps\":{\"alert\":{\"body\":\"a body\"},\"mutable-content\":0}}",
279 /// &payload.to_json_string().unwrap()
280 /// );
281 /// # }
282 /// ```
283 pub fn body(mut self, body: impl Into<Cow<'a, str>>) -> Self {
284 self.alert.body = Some(body.into());
285 self
286 }
287
288 #[deprecated(
289 since = "0.11.0",
290 note = "Use the idiomatic `body` instead of the legacy `set_*` fn"
291 )]
292 pub fn set_body(self, body: impl Into<Cow<'a, str>>) -> Self {
293 self.body(body)
294 }
295
296 /// A number to show on a badge on top of the app icon.
297 ///
298 /// ```rust
299 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
300 /// # use apns_h2::request::payload::PayloadLike;
301 /// # fn main() {
302 /// let mut builder = DefaultNotificationBuilder::new()
303 /// .badge(4);
304 /// let payload = builder.build("token", Default::default());
305 ///
306 /// assert_eq!(
307 /// "{\"aps\":{\"badge\":4,\"mutable-content\":0}}",
308 /// &payload.to_json_string().unwrap()
309 /// );
310 /// # }
311 /// ```
312 pub fn badge(mut self, badge: u32) -> Self {
313 self.badge = Some(badge);
314 self
315 }
316
317 #[deprecated(
318 since = "0.11.0",
319 note = "Use the idiomatic `badge` instead of the legacy `set_*` fn"
320 )]
321 pub fn set_badge(self, badge: u32) -> Self {
322 self.badge(badge)
323 }
324
325 /// File name of the custom sound to play when receiving the notification.
326 ///
327 /// ```rust
328 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
329 /// # use apns_h2::request::payload::PayloadLike;
330 /// # fn main() {
331 /// let mut builder = DefaultNotificationBuilder::new()
332 /// .title("a title")
333 /// .sound("ping");
334 /// let payload = builder.build("token", Default::default());
335 ///
336 /// assert_eq!(
337 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"sound\":\"ping\",\"mutable-content\":0}}",
338 /// &payload.to_json_string().unwrap()
339 /// );
340 /// # }
341 /// ```
342 pub fn sound(mut self, sound: impl Into<Cow<'a, str>>) -> Self {
343 self.sound.name = Some(sound.into());
344 self
345 }
346
347 #[deprecated(
348 since = "0.11.0",
349 note = "Use the idiomatic `sound` instead of the legacy `set_*` fn"
350 )]
351 pub fn set_sound(self, sound: impl Into<Cow<'a, str>>) -> Self {
352 self.sound(sound)
353 }
354
355 /// An application-specific name that allows notifications to be grouped together.
356 ///
357 /// ```rust
358 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
359 /// # use apns_h2::request::payload::PayloadLike;
360 /// # fn main() {
361 /// let mut builder = DefaultNotificationBuilder::new()
362 /// .title("a title")
363 /// .thread_id("my-thread");
364 /// let payload = builder.build("token", Default::default());
365 ///
366 /// assert_eq!(
367 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"thread-id\":\"my-thread\",\"mutable-content\":0}}",
368 /// &payload.to_json_string().unwrap()
369 /// );
370 /// # }
371 /// ```
372 pub fn thread_id(mut self, thread_id: impl Into<Cow<'a, str>>) -> Self {
373 self.thread_id = Some(thread_id.into());
374 self
375 }
376
377 /// When a notification includes the category key, the system displays the
378 /// actions for that category as buttons in the banner or alert interface.
379 ///
380 /// ```rust
381 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
382 /// # use apns_h2::request::payload::PayloadLike;
383 /// # fn main() {
384 /// let mut builder = DefaultNotificationBuilder::new()
385 /// .title("a title")
386 /// .category("cat1");
387 /// let payload = builder.build("token", Default::default());
388 ///
389 /// assert_eq!(
390 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"category\":\"cat1\",\"mutable-content\":0}}",
391 /// &payload.to_json_string().unwrap()
392 /// );
393 /// # }
394 /// ```
395 pub fn category(mut self, category: impl Into<Cow<'a, str>>) -> Self {
396 self.category = Some(category.into());
397 self
398 }
399
400 #[deprecated(
401 since = "0.11.0",
402 note = "Use the idiomatic `category` instead of the legacy `set_*` fn"
403 )]
404 pub fn set_category(self, category: impl Into<Cow<'a, str>>) -> Self {
405 self.category(category)
406 }
407
408 /// The subtitle localization key for the notification title.
409 ///
410 /// ```rust
411 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
412 /// # use apns_h2::request::payload::PayloadLike;
413 /// # fn main() {
414 /// let mut builder = DefaultNotificationBuilder::new()
415 /// .title("a title")
416 /// .subtitle_loc_key("yolo");
417 /// let payload = builder.build("token", Default::default());
418 ///
419 /// assert_eq!(
420 /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"subtitle-loc-key\":\"yolo\"},\"mutable-content\":0}}",
421 /// &payload.to_json_string().unwrap()
422 /// );
423 /// # }
424 /// ```
425 pub fn subtitle_loc_key(mut self, key: impl Into<Cow<'a, str>>) -> Self {
426 self.alert.subtitle_loc_key = Some(key.into());
427 self
428 }
429
430 /// Arguments for the title localization.
431 ///
432 /// ```rust
433 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
434 /// # use apns_h2::request::payload::PayloadLike;
435 /// # fn main() {
436 /// let mut builder = DefaultNotificationBuilder::new()
437 /// .title("a title")
438 /// .subtitle_loc_args(&["fooz", "barz"]);
439 /// let payload = builder.build("token", Default::default());
440 ///
441 /// assert_eq!(
442 /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"subtitle-loc-args\":[\"fooz\",\"barz\"]},\"mutable-content\":0}}",
443 /// &payload.to_json_string().unwrap()
444 /// );
445 /// # }
446 /// ```
447 pub fn subtitle_loc_args<S>(mut self, args: &'a [S]) -> Self
448 where
449 S: Into<Cow<'a, str>> + AsRef<str>,
450 {
451 let converted = args.iter().map(AsRef::as_ref).map(Into::into).collect();
452
453 self.alert.subtitle_loc_args = Some(converted);
454 self
455 }
456
457 /// The localization key for the notification title.
458 ///
459 /// ```rust
460 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
461 /// # use apns_h2::request::payload::PayloadLike;
462 /// # fn main() {
463 /// let mut builder = DefaultNotificationBuilder::new()
464 /// .title("a title")
465 /// .title_loc_key("play");
466 /// let payload = builder.build("token", Default::default());
467 ///
468 /// assert_eq!(
469 /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"title-loc-key\":\"play\"},\"mutable-content\":0}}",
470 /// &payload.to_json_string().unwrap()
471 /// );
472 /// # }
473 /// ```
474 pub fn title_loc_key(mut self, key: impl Into<Cow<'a, str>>) -> Self {
475 self.alert.title_loc_key = Some(key.into());
476 self
477 }
478
479 #[deprecated(
480 since = "0.11.0",
481 note = "Use the idiomatic `title_loc_key` instead of the legacy `set_*` fn"
482 )]
483 pub fn set_title_loc_key(self, key: impl Into<Cow<'a, str>>) -> Self {
484 self.title_loc_key(key)
485 }
486
487 /// Arguments for the title localization.
488 ///
489 /// ```rust
490 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
491 /// # use apns_h2::request::payload::PayloadLike;
492 /// # fn main() {
493 /// let mut builder = DefaultNotificationBuilder::new()
494 /// .title("a title")
495 /// .title_loc_args(&["foo", "bar"]);
496 /// let payload = builder.build("token", Default::default());
497 ///
498 /// assert_eq!(
499 /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"title-loc-args\":[\"foo\",\"bar\"]},\"mutable-content\":0}}",
500 /// &payload.to_json_string().unwrap()
501 /// );
502 /// # }
503 /// ```
504 pub fn title_loc_args<S>(mut self, args: &'a [S]) -> Self
505 where
506 S: Into<Cow<'a, str>> + AsRef<str>,
507 {
508 let converted = args.iter().map(AsRef::as_ref).map(Into::into).collect();
509
510 self.alert.title_loc_args = Some(converted);
511 self
512 }
513
514 #[deprecated(
515 since = "0.11.0",
516 note = "Use the idiomatic `title_loc_args` instead of the legacy `set_*` fn"
517 )]
518 pub fn set_title_loc_args<S>(self, key: &'a [S]) -> Self
519 where
520 S: Into<Cow<'a, str>> + AsRef<str>,
521 {
522 self.title_loc_args(key)
523 }
524
525 /// The localization key for the action.
526 ///
527 /// ```rust
528 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
529 /// # use apns_h2::request::payload::PayloadLike;
530 /// # fn main() {
531 /// let mut builder = DefaultNotificationBuilder::new()
532 /// .title("a title")
533 /// .action_loc_key("stop");
534 /// let payload = builder.build("token", Default::default());
535 ///
536 /// assert_eq!(
537 /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"action-loc-key\":\"stop\"},\"mutable-content\":0}}",
538 /// &payload.to_json_string().unwrap()
539 /// );
540 /// # }
541 /// ```
542 pub fn action_loc_key(mut self, key: impl Into<Cow<'a, str>>) -> Self {
543 self.alert.action_loc_key = Some(key.into());
544 self
545 }
546
547 #[deprecated(
548 since = "0.11.0",
549 note = "Use the idiomatic `action_loc_key` instead of the legacy `set_*` fn"
550 )]
551 pub fn set_action_loc_key(self, key: impl Into<Cow<'a, str>>) -> Self {
552 self.action_loc_key(key)
553 }
554
555 /// The localization key for the push message body.
556 ///
557 /// ```rust
558 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
559 /// # use apns_h2::request::payload::PayloadLike;
560 /// # fn main() {
561 /// let mut builder = DefaultNotificationBuilder::new()
562 /// .title("a title")
563 /// .loc_key("lol");
564 /// let payload = builder.build("token", Default::default());
565 ///
566 /// assert_eq!(
567 /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"loc-key\":\"lol\"},\"mutable-content\":0}}",
568 /// &payload.to_json_string().unwrap()
569 /// );
570 /// # }
571 /// ```
572 pub fn loc_key(mut self, key: impl Into<Cow<'a, str>>) -> Self {
573 self.alert.loc_key = Some(key.into());
574 self
575 }
576
577 #[deprecated(
578 since = "0.11.0",
579 note = "Use the idiomatic `loc_key` instead of the legacy `set_*` fn"
580 )]
581 pub fn set_loc_key(self, key: impl Into<Cow<'a, str>>) -> Self {
582 self.loc_key(key)
583 }
584
585 /// Arguments for the content localization.
586 ///
587 /// ```rust
588 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
589 /// # use apns_h2::request::payload::PayloadLike;
590 /// # fn main() {
591 /// let mut builder = DefaultNotificationBuilder::new()
592 /// .title("a title")
593 /// .loc_args(&["omg", "foo"]);
594 /// let payload = builder.build("token", Default::default());
595 ///
596 /// assert_eq!(
597 /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"loc-args\":[\"omg\",\"foo\"]},\"mutable-content\":0}}",
598 /// &payload.to_json_string().unwrap()
599 /// );
600 /// # }
601 /// ```
602 pub fn loc_args<S>(mut self, args: &'a [S]) -> Self
603 where
604 S: Into<Cow<'a, str>> + AsRef<str>,
605 {
606 let converted = args.iter().map(|a| a.as_ref().into()).collect();
607
608 self.alert.loc_args = Some(converted);
609 self
610 }
611
612 #[deprecated(
613 since = "0.11.0",
614 note = "Use the idiomatic `loc_args` instead of the legacy `set_*` fn"
615 )]
616 pub fn set_loc_args<S>(self, key: &'a [S]) -> Self
617 where
618 S: Into<Cow<'a, str>> + AsRef<str>,
619 {
620 self.loc_args(key)
621 }
622
623 /// Image to display in the rich notification.
624 ///
625 /// ```rust
626 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
627 /// # use apns_h2::request::payload::PayloadLike;
628 /// # fn main() {
629 /// let mut builder = DefaultNotificationBuilder::new()
630 /// .title("a title")
631 /// .launch_image("cat.png");
632 /// let payload = builder.build("token", Default::default());
633 ///
634 /// assert_eq!(
635 /// "{\"aps\":{\"alert\":{\"title\":\"a title\",\"launch-image\":\"cat.png\"},\"mutable-content\":0}}",
636 /// &payload.to_json_string().unwrap()
637 /// );
638 /// # }
639 /// ```
640 pub fn launch_image(mut self, image: impl Into<Cow<'a, str>>) -> Self {
641 self.alert.launch_image = Some(image.into());
642 self
643 }
644
645 #[deprecated(
646 since = "0.11.0",
647 note = "Use the idiomatic `launch_image` instead of the legacy `set_*` fn"
648 )]
649 pub fn set_launch_image(self, image: impl Into<Cow<'a, str>>) -> Self {
650 self.launch_image(image)
651 }
652
653 /// Allow client to modify push content before displaying.
654 ///
655 /// ```rust
656 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
657 /// # use apns_h2::request::payload::PayloadLike;
658 /// # fn main() {
659 /// let mut builder = DefaultNotificationBuilder::new()
660 /// .title("a title")
661 /// .mutable_content();
662 /// let payload = builder.build("token", Default::default());
663 ///
664 /// assert_eq!(
665 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":1}}",
666 /// &payload.to_json_string().unwrap()
667 /// );
668 /// # }
669 /// ```
670 pub fn mutable_content(mut self) -> Self {
671 self.mutable_content = 1;
672 self
673 }
674
675 #[deprecated(
676 since = "0.11.0",
677 note = "Use the idiomatic `mutable_content` instead of the legacy `set_*` fn"
678 )]
679 pub fn set_mutable_content(self) -> Self {
680 self.mutable_content()
681 }
682
683 /// Used for adding custom data to push notifications
684 ///
685 /// ```rust
686 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
687 /// # use apns_h2::request::payload::PayloadLike;
688 /// # fn main() {
689 /// let mut builder = DefaultNotificationBuilder::new()
690 /// .title("a title")
691 /// .content_available();
692 /// let payload = builder.build("token", Default::default());
693 ///
694 /// assert_eq!(
695 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"content-available\":1,\"mutable-content\":0}}",
696 /// &payload.to_json_string().unwrap()
697 /// );
698 /// # }
699 /// ```
700 pub fn content_available(mut self) -> Self {
701 self.content_available = Some(1);
702 self
703 }
704
705 #[deprecated(
706 since = "0.11.0",
707 note = "Use the idiomatic `content_available` instead of the legacy `set_*` fn"
708 )]
709 pub fn set_content_available(self) -> Self {
710 self.content_available()
711 }
712
713 /// Set the interruption level to active. The system presents the notification
714 /// immediately, lights up the screen, and can play a sound.
715 ///
716 /// ```rust
717 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
718 /// # use apns_h2::request::payload::PayloadLike;
719 /// # fn main() {
720 /// let mut builder = DefaultNotificationBuilder::new()
721 /// .title("a title")
722 /// .active_interruption_level();
723 /// let payload = builder.build("token", Default::default());
724 ///
725 /// assert_eq!(
726 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"active\"}}",
727 /// &payload.to_json_string().unwrap()
728 /// );
729 /// # }
730 /// ```
731 pub fn active_interruption_level(mut self) -> Self {
732 self.interruption_level = Some(InterruptionLevel::Active);
733 self
734 }
735
736 /// Set the interruption level to critical. The system presents the notification
737 /// immediately, lights up the screen, and bypasses the mute switch to play a sound.
738 ///
739 /// ```rust
740 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
741 /// # use apns_h2::request::payload::PayloadLike;
742 /// # fn main() {
743 /// let mut builder = DefaultNotificationBuilder::new()
744 /// .title("a title")
745 /// .critical_interruption_level();
746 /// let payload = builder.build("token", Default::default());
747 ///
748 /// assert_eq!(
749 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"critical\"}}",
750 /// &payload.to_json_string().unwrap()
751 /// );
752 /// # }
753 /// ```
754 pub fn critical_interruption_level(mut self) -> Self {
755 self.interruption_level = Some(InterruptionLevel::Critical);
756 self
757 }
758
759 /// Set the interruption level to passive. The system adds the notification to
760 /// the notification list without lighting up the screen or playing a sound.
761 ///
762 /// ```rust
763 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
764 /// # use apns_h2::request::payload::PayloadLike;
765 /// # fn main() {
766 /// let mut builder = DefaultNotificationBuilder::new()
767 /// .title("a title")
768 /// .passive_interruption_level();
769 /// let payload = builder.build("token", Default::default());
770 ///
771 /// assert_eq!(
772 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"passive\"}}",
773 /// &payload.to_json_string().unwrap()
774 /// );
775 /// # }
776 /// ```
777 pub fn passive_interruption_level(mut self) -> Self {
778 self.interruption_level = Some(InterruptionLevel::Passive);
779 self
780 }
781
782 /// Set the interruption level to time sensitive. The system presents the notification
783 /// immediately, lights up the screen, can play a sound, and breaks through system
784 /// notification controls.
785 ///
786 /// ```rust
787 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
788 /// # use apns_h2::request::payload::PayloadLike;
789 /// # fn main() {
790 /// let mut builder = DefaultNotificationBuilder::new()
791 /// .title("a title")
792 /// .time_sensitive_interruption_level();
793 /// let payload = builder.build("token", Default::default());
794 ///
795 /// assert_eq!(
796 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"time-sensitive\"}}",
797 /// &payload.to_json_string().unwrap()
798 /// );
799 /// # }
800 /// ```
801 pub fn time_sensitive_interruption_level(mut self) -> Self {
802 self.interruption_level = Some(InterruptionLevel::TimeSensitive);
803 self
804 }
805
806 /// Set the interruption level directly. Controls how the notification is presented to the user.
807 ///
808 /// ```rust
809 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
810 /// # use apns_h2::request::payload::{PayloadLike, InterruptionLevel};
811 /// # fn main() {
812 /// let mut builder = DefaultNotificationBuilder::new()
813 /// .title("a title")
814 /// .interruption_level(InterruptionLevel::Active);
815 /// let payload = builder.build("token", Default::default());
816 ///
817 /// assert_eq!(
818 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"active\"}}",
819 /// &payload.to_json_string().unwrap()
820 /// );
821 /// # }
822 /// ```
823 pub fn interruption_level(mut self, level: InterruptionLevel) -> Self {
824 self.interruption_level = Some(level);
825 self
826 }
827
828 /// Set the timestamp for a Live Activity update
829 ///
830 /// ```rust
831 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
832 /// # use apns_h2::request::payload::PayloadLike;
833 /// # fn main() {
834 /// let payload = DefaultNotificationBuilder::new()
835 /// .timestamp(1234)
836 /// .build("token", Default::default());
837 ///
838 /// assert_eq!(
839 /// "{\"aps\":{\"mutable-content\":0,\"timestamp\":1234}}",
840 /// &payload.to_json_string().unwrap()
841 /// );
842 /// # }
843 /// ```
844 pub fn timestamp(mut self, timestamp: u64) -> Self {
845 self.timestamp = Some(timestamp);
846 self
847 }
848
849 /// Set the event for a Live Activity. Use "start" to begin a Live Activity.
850 ///
851 /// ```rust
852 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
853 /// # use apns_h2::request::payload::PayloadLike;
854 /// # fn main() {
855 /// let payload = DefaultNotificationBuilder::new()
856 /// .event("start")
857 /// .build("token", Default::default());
858 ///
859 /// assert_eq!(
860 /// "{\"aps\":{\"mutable-content\":0,\"event\":\"start\"}}",
861 /// &payload.to_json_string().unwrap()
862 /// );
863 /// # }
864 /// ```
865 pub fn event(mut self, event: impl Into<Cow<'a, str>>) -> Self {
866 self.event = Some(event.into());
867 self
868 }
869
870 /// Set the content state for a Live Activity with dynamic data
871 ///
872 /// ```rust
873 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
874 /// # use apns_h2::request::payload::PayloadLike;
875 /// # use serde_json::json;
876 /// # fn main() {
877 /// let content_state = json!({
878 /// "currentHealthLevel": 100,
879 /// "eventDescription": "Adventure has begun!"
880 /// });
881 /// let payload = DefaultNotificationBuilder::new()
882 /// .content_state(&content_state)
883 /// .build("token", Default::default());
884 ///
885 /// assert!(payload.to_json_string().unwrap().contains("\"content-state\":{\"currentHealthLevel\":100,\"eventDescription\":\"Adventure has begun!\"}"));
886 /// # }
887 /// ```
888 pub fn content_state(mut self, content_state: &serde_json::Value) -> Self {
889 self.content_state = Some(content_state.clone());
890 self
891 }
892
893 /// Set the attributes type for a Live Activity
894 ///
895 /// ```rust
896 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
897 /// # use apns_h2::request::payload::PayloadLike;
898 /// # fn main() {
899 /// let payload = DefaultNotificationBuilder::new()
900 /// .attributes_type("AdventureAttributes")
901 /// .build("token", Default::default());
902 ///
903 /// assert_eq!(
904 /// "{\"aps\":{\"mutable-content\":0,\"attributes-type\":\"AdventureAttributes\"}}",
905 /// &payload.to_json_string().unwrap()
906 /// );
907 /// # }
908 /// ```
909 pub fn attributes_type(mut self, attributes_type: impl Into<Cow<'a, str>>) -> Self {
910 self.attributes_type = Some(attributes_type.into());
911 self
912 }
913
914 /// Set the attributes for a Live Activity with data defining the Live Activity
915 ///
916 /// ```rust
917 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
918 /// # use apns_h2::request::payload::PayloadLike;
919 /// # use serde_json::json;
920 /// # fn main() {
921 /// let attributes = json!({
922 /// "currentHealthLevel": 100,
923 /// "eventDescription": "Adventure has begun!"
924 /// });
925 /// let payload = DefaultNotificationBuilder::new()
926 /// .attributes(&attributes)
927 /// .build("token", Default::default());
928 ///
929 /// assert!(payload.to_json_string().unwrap().contains("\"attributes\":{\"currentHealthLevel\":100,\"eventDescription\":\"Adventure has begun!\"}"));
930 /// # }
931 /// ```
932 pub fn attributes(mut self, attributes: &serde_json::Value) -> Self {
933 self.attributes = Some(attributes.clone());
934 self
935 }
936
937 /// Set the input push channel ID for iOS 18+ channel-based Live Activity updates
938 ///
939 /// ```rust
940 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
941 /// # use apns_h2::request::payload::PayloadLike;
942 /// # fn main() {
943 /// let payload = DefaultNotificationBuilder::new()
944 /// .input_push_channel("dHN0LXNyY2gtY2hubA==")
945 /// .build("token", Default::default());
946 ///
947 /// assert_eq!(
948 /// "{\"aps\":{\"mutable-content\":0,\"input-push-channel\":\"dHN0LXNyY2gtY2hubA==\"}}",
949 /// &payload.to_json_string().unwrap()
950 /// );
951 /// # }
952 /// ```
953 pub fn input_push_channel(mut self, channel_id: impl Into<Cow<'a, str>>) -> Self {
954 self.input_push_channel = Some(channel_id.into());
955 self
956 }
957
958 /// Enable input push token request for iOS 18+ token-based Live Activity updates
959 ///
960 /// ```rust
961 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
962 /// # use apns_h2::request::payload::PayloadLike;
963 /// # fn main() {
964 /// let payload = DefaultNotificationBuilder::new()
965 /// .input_push_token()
966 /// .build("token", Default::default());
967 ///
968 /// assert_eq!(
969 /// "{\"aps\":{\"mutable-content\":0,\"input-push-token\":1}}",
970 /// &payload.to_json_string().unwrap()
971 /// );
972 /// # }
973 /// ```
974 pub fn input_push_token(mut self) -> Self {
975 self.input_push_token = Some(1);
976 self
977 }
978
979 /// Set the dismissal date for when the system should automatically remove the notification.
980 /// The timestamp should be in Unix epoch time (seconds since 1970-01-01 00:00:00 UTC).
981 ///
982 /// ```rust
983 /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
984 /// # use apns_h2::request::payload::PayloadLike;
985 /// # fn main() {
986 /// let payload = DefaultNotificationBuilder::new()
987 /// .title("a title")
988 /// .dismissal_date(1672531200) // January 1, 2023 00:00:00 UTC
989 /// .build("token", Default::default());
990 ///
991 /// assert_eq!(
992 /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"dismissal-date\":1672531200}}",
993 /// &payload.to_json_string().unwrap()
994 /// );
995 /// # }
996 /// ```
997 pub fn dismissal_date(mut self, dismissal_date: u64) -> Self {
998 self.dismissal_date = Some(dismissal_date);
999 self
1000 }
1001}
1002
1003impl<'a> NotificationBuilder<'a> for DefaultNotificationBuilder<'a> {
1004 fn build(self, device_token: impl Into<Cow<'a, str>>, options: NotificationOptions<'a>) -> Payload<'a> {
1005 use std::sync::OnceLock;
1006
1007 static DEFAULT_ALERT: OnceLock<DefaultAlert<'static>> = OnceLock::new();
1008
1009 Payload {
1010 aps: APS {
1011 alert: if &self.alert == DEFAULT_ALERT.get_or_init(Default::default) {
1012 None
1013 } else {
1014 Some(APSAlert::Default(Box::new(self.alert)))
1015 },
1016 badge: self.badge,
1017 sound: if self.sound.critical {
1018 Some(APSSound::Critical(self.sound))
1019 } else {
1020 self.sound.name.map(APSSound::Sound)
1021 },
1022 thread_id: self.thread_id,
1023 content_available: self.content_available,
1024 category: self.category,
1025 mutable_content: Some(self.mutable_content),
1026 interruption_level: self.interruption_level,
1027 dismissal_date: self.dismissal_date,
1028 url_args: None,
1029 timestamp: self.timestamp,
1030 event: self.event,
1031 content_state: self.content_state,
1032 attributes_type: self.attributes_type,
1033 attributes: self.attributes,
1034 input_push_channel: self.input_push_channel,
1035 input_push_token: self.input_push_token,
1036 },
1037 device_token: device_token.into(),
1038 options,
1039 data: BTreeMap::new(),
1040 }
1041 }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047 use serde_json::value::to_value;
1048
1049 #[test]
1050 fn test_default_notification_with_minimal_required_values() {
1051 let payload = DefaultNotificationBuilder::new()
1052 .title("the title")
1053 .body("the body")
1054 .build("device-token", Default::default());
1055
1056 let expected_payload = json!({
1057 "aps": {
1058 "alert": {
1059 "body": "the body",
1060 "title": "the title",
1061 },
1062 "mutable-content": 0
1063 }
1064 });
1065
1066 assert_eq!(expected_payload, to_value(payload).unwrap());
1067 }
1068
1069 #[test]
1070 fn test_default_notification_with_dismissal_date() {
1071 let builder = DefaultNotificationBuilder::new()
1072 .title("Test Title")
1073 .body("Test Body")
1074 .dismissal_date(1672531200); // January 1, 2023 00:00:00 UTC
1075
1076 let payload = builder.build("device-token", Default::default());
1077
1078 let expected_payload = json!({
1079 "aps": {
1080 "alert": {
1081 "title": "Test Title",
1082 "body": "Test Body"
1083 },
1084 "dismissal-date": 1672531200,
1085 "mutable-content": 0
1086 }
1087 });
1088
1089 assert_eq!(expected_payload, to_value(payload).unwrap());
1090 }
1091
1092 #[test]
1093 fn test_loc_args_inputs() {
1094 let owned_strings: Vec<String> = vec!["hello".to_string(), "world".to_string()];
1095 let borrowed_strings: Vec<&str> = vec!["foo", "bar"];
1096 let slice_strings: &[&str] = &["baz", "qux"];
1097 let owned_cows: Vec<Cow<'static, str>> = vec![Cow::Borrowed("narf"), Cow::Owned("derp".to_string())];
1098 let builder = DefaultNotificationBuilder::new()
1099 .loc_args(&owned_strings)
1100 .loc_args(&borrowed_strings)
1101 .loc_args(slice_strings)
1102 .loc_args(&owned_cows);
1103
1104 let payload = builder.build("device-token", Default::default());
1105
1106 let expected_payload = json!({
1107 "aps": {
1108 "alert": {
1109 "loc-args": ["narf", "derp"],
1110 },
1111 "mutable-content": 0,
1112 }
1113 });
1114
1115 assert_eq!(expected_payload, to_value(payload).unwrap());
1116 }
1117
1118 #[test]
1119 fn test_default_notification_with_full_data() {
1120 let builder = DefaultNotificationBuilder::new()
1121 .title("the title")
1122 .body("the body")
1123 .badge(420)
1124 .category("cat1")
1125 .sound("prööt")
1126 .critical(true, Some(1.0))
1127 .mutable_content()
1128 .action_loc_key("PLAY")
1129 .launch_image("foo.jpg")
1130 .loc_args(&["argh", "narf"])
1131 .title_loc_key("STOP")
1132 .title_loc_args(&["herp", "derp"])
1133 .loc_key("PAUSE")
1134 .loc_args(&["narf", "derp"]);
1135
1136 let payload = builder.build("device-token", Default::default());
1137
1138 let expected_payload = json!({
1139 "aps": {
1140 "alert": {
1141 "action-loc-key": "PLAY",
1142 "body": "the body",
1143 "launch-image": "foo.jpg",
1144 "loc-args": ["narf", "derp"],
1145 "loc-key": "PAUSE",
1146 "title": "the title",
1147 "title-loc-args": ["herp", "derp"],
1148 "title-loc-key": "STOP"
1149 },
1150 "badge": 420,
1151 "sound": {
1152 "critical": 1,
1153 "name": "prööt",
1154 "volume": 1.0,
1155 },
1156 "category": "cat1",
1157 "mutable-content": 1,
1158 }
1159 });
1160
1161 assert_eq!(expected_payload, to_value(payload).unwrap());
1162 }
1163
1164 #[test]
1165 fn test_notification_with_custom_data_1() {
1166 #[derive(Serialize, Debug)]
1167 struct SubData {
1168 nothing: &'static str,
1169 }
1170
1171 #[derive(Serialize, Debug)]
1172 struct TestData {
1173 key_str: &'static str,
1174 key_num: u32,
1175 key_bool: bool,
1176 key_struct: SubData,
1177 }
1178
1179 let test_data = TestData {
1180 key_str: "foo",
1181 key_num: 42,
1182 key_bool: false,
1183 key_struct: SubData { nothing: "here" },
1184 };
1185
1186 let mut payload = DefaultNotificationBuilder::new()
1187 .title("the title")
1188 .body("the body")
1189 .build("device-token", Default::default());
1190
1191 payload.add_custom_data("custom", &test_data).unwrap();
1192
1193 let expected_payload = json!({
1194 "custom": {
1195 "key_str": "foo",
1196 "key_num": 42,
1197 "key_bool": false,
1198 "key_struct": {
1199 "nothing": "here"
1200 }
1201 },
1202 "aps": {
1203 "alert": {
1204 "body": "the body",
1205 "title": "the title",
1206 },
1207 "mutable-content": 0,
1208 },
1209 });
1210
1211 assert_eq!(expected_payload, to_value(payload).unwrap());
1212 }
1213
1214 #[test]
1215 fn test_notification_with_custom_data_2() {
1216 #[derive(Serialize, Debug)]
1217 struct SubData {
1218 nothing: &'static str,
1219 }
1220
1221 #[derive(Serialize, Debug)]
1222 struct TestData {
1223 key_str: &'static str,
1224 key_num: u32,
1225 key_bool: bool,
1226 key_struct: SubData,
1227 }
1228
1229 let test_data = TestData {
1230 key_str: "foo",
1231 key_num: 42,
1232 key_bool: false,
1233 key_struct: SubData { nothing: "here" },
1234 };
1235
1236 let mut payload = DefaultNotificationBuilder::new()
1237 .body("kulli")
1238 .build("device-token", Default::default());
1239
1240 payload.add_custom_data("custom", &test_data).unwrap();
1241
1242 let expected_payload = json!({
1243 "custom": {
1244 "key_str": "foo",
1245 "key_num": 42,
1246 "key_bool": false,
1247 "key_struct": {
1248 "nothing": "here"
1249 }
1250 },
1251 "aps": {
1252 "alert": {
1253 "body": "kulli"
1254 },
1255 "mutable-content": 0
1256 }
1257 });
1258
1259 assert_eq!(expected_payload, to_value(payload).unwrap());
1260 }
1261
1262 #[test]
1263 fn test_silent_notification_with_no_content() {
1264 let payload = DefaultNotificationBuilder::new()
1265 .content_available()
1266 .build("device-token", Default::default());
1267
1268 let expected_payload = json!({
1269 "aps": {
1270 "content-available": 1,
1271 "mutable-content": 0
1272 }
1273 });
1274
1275 assert_eq!(expected_payload, to_value(payload).unwrap());
1276 }
1277
1278 #[test]
1279 fn test_silent_notification_with_custom_data() {
1280 #[derive(Serialize, Debug)]
1281 struct SubData {
1282 nothing: &'static str,
1283 }
1284
1285 #[derive(Serialize, Debug)]
1286 struct TestData {
1287 key_str: &'static str,
1288 key_num: u32,
1289 key_bool: bool,
1290 key_struct: SubData,
1291 }
1292
1293 let test_data = TestData {
1294 key_str: "foo",
1295 key_num: 42,
1296 key_bool: false,
1297 key_struct: SubData { nothing: "here" },
1298 };
1299
1300 let mut payload = DefaultNotificationBuilder::new()
1301 .content_available()
1302 .build("device-token", Default::default());
1303
1304 payload.add_custom_data("custom", &test_data).unwrap();
1305
1306 let expected_payload = json!({
1307 "aps": {
1308 "content-available": 1,
1309 "mutable-content": 0
1310 },
1311 "custom": {
1312 "key_str": "foo",
1313 "key_num": 42,
1314 "key_bool": false,
1315 "key_struct": {
1316 "nothing": "here"
1317 }
1318 }
1319 });
1320
1321 assert_eq!(expected_payload, to_value(payload).unwrap());
1322 }
1323
1324 #[test]
1325 fn test_silent_notification_with_custom_hashmap() {
1326 let mut test_data = BTreeMap::new();
1327 test_data.insert("key_str", "foo");
1328 test_data.insert("key_str2", "bar");
1329
1330 let mut payload = DefaultNotificationBuilder::new()
1331 .content_available()
1332 .build("device-token", Default::default());
1333
1334 payload.add_custom_data("custom", &test_data).unwrap();
1335
1336 let expected_payload = json!({
1337 "aps": {
1338 "content-available": 1,
1339 "mutable-content": 0,
1340 },
1341 "custom": {
1342 "key_str": "foo",
1343 "key_str2": "bar"
1344 }
1345 });
1346
1347 assert_eq!(expected_payload, to_value(payload).unwrap());
1348 }
1349}