├── CHANGELOG.md ├── .formatter.exs ├── .gitignore ├── .travis.yml ├── NOTICE ├── lib ├── exponent_server_sdk.ex └── exponent_server_sdk │ ├── parser.ex │ ├── push_notification.ex │ └── push_message.ex ├── bin ├── setup ├── release └── test ├── CONTRIBUTING.md ├── test ├── test_helper.exs └── exponent_server_sdk │ ├── parser_test.exs │ ├── push_message_test.exs │ └── push_notification_test.exs ├── LICENSE ├── mix.exs ├── README.md └── mix.lock /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | .env 6 | .DS_Store 7 | doc/ 8 | .elixir_ls 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | cache: 3 | directories: 4 | - $HOME/.mix 5 | elixir: 6 | - 1.7.2 7 | otp_release: 8 | - 20.1 9 | before_script: 10 | - export MIX_HOME=$HOME/.mix 11 | - export MIX_ENV=test 12 | - chmod +x bin/setup 13 | - chmod +x bin/test 14 | script: 15 | - bin/setup 16 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Nathan Stool, 2018. 2 | 3 | This product depends on the following third-party components: 4 | 5 | * elixir (https://github.com/elixir-lang/elixir) 6 | 7 | Copyright 2013-2014 Plataformatec. Elixir is open source under the 8 | Apache 2.0 license, available here: 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | -------------------------------------------------------------------------------- /lib/exponent_server_sdk.ex: -------------------------------------------------------------------------------- 1 | defmodule ExponentServerSdk do 2 | @moduledoc """ 3 | ExponentServerSdk is a relatively simple-featured API client for the Exponent Push Notification API. 4 | 5 | Take a look at the Resource module `ExponentServerSdk.PushNotification`. 6 | 7 | If you want to learn more about how ExponentServerSdk works internally, take a dive into 8 | at: 9 | - `ExponentServerSdk.Parser` 10 | - `ExponentServerSdk.PushMessage` 11 | """ 12 | end 13 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script sets up your local machine for development on ExponentServerSdk. 4 | 5 | echo "-------------------------------" 6 | echo "Installing dependencies..." 7 | echo "-------------------------------" 8 | 9 | mix local.hex --force || { echo "Could not install Hex!"; exit 1; } 10 | mix deps.get --only test || { echo "Could not install dependencies!"; exit 1;} 11 | 12 | echo "-------------------------------" 13 | echo "Running tests..." 14 | echo "-------------------------------" 15 | 16 | bin/test || { exit 1; } 17 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | puts "What version number do you want to release?" 4 | print "> " 5 | version = gets.gsub(/\n/, "") 6 | 7 | continue = system "github_changelog_generator" 8 | continue = system "git add ." 9 | continue = system "git commit -am \"Release version #{version}\"" if continue 10 | continue = system "git tag v#{version}" if continue 11 | continue = system "git push" if continue 12 | continue = system "git push -f origin v#{version}" if continue 13 | continue = system "mix hex.publish" if continue 14 | 15 | puts "Version #{version} was successfully released!" 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | I welcome contributions to ExponentServerSdk. Contributors should keep in mind the following rules: 4 | 5 | - Pull requests will not be accepted if the Travis build is failing. 6 | - Code changes should be accompanied with test changes or additions to validate that your changes actually work. 7 | - Code changes must update inline documentation in all relevant areas. Users rely on this documentation. 8 | - Any code you submit will be subject to the MIT license. 9 | - Discussion must be courteous at all times. Offending discussions will be closed and locked. 10 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule TestHelper do 4 | use ExUnit.Case, async: false 5 | alias ExponentServerSdk.PushNotification 6 | import Mock 7 | 8 | def with_fixture(:get!, response, fun), 9 | do: with_fixture({:get!, fn _url, _headers -> response end}, fun) 10 | 11 | def with_fixture(:post!, response, fun), 12 | do: with_fixture({:post!, fn _url, _options, _headers -> response end}, fun) 13 | 14 | def with_fixture(stub, fun) do 15 | with_mock PushNotification, [:passthrough], [stub] do 16 | fun.() 17 | end 18 | end 19 | 20 | def json_response(map, status) do 21 | {:ok, json} = Poison.encode(map) 22 | %{body: json, status_code: status} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Continuous Integration Script 4 | # 5 | # This script contains all the test commands for this app, and should be run on 6 | # your continuous integration server. Developers can then run it locally to see 7 | # if their changes will pass on the CI server, before pushing. 8 | # 9 | # It also allows the build settings to be changed by anyone with write access to 10 | # the project repo, making them easier to manage. 11 | 12 | MIX_ENV=test mix format --check-formatted || { echo 'Please format code with `mix format`.'; exit 1; } 13 | 14 | MIX_ENV=test mix compile --warnings-as-errors --force || { echo 'Please fix all compiler warnings.'; exit 1; } 15 | 16 | MIX_ENV=test mix credo --strict --ignore design,consistency || { echo 'Elixir code failed Credo linting. See warnings above.'; exit 1; } 17 | 18 | MIX_ENV=test mix docs || { echo 'Elixir HTML docs were not generated!'; exit 1; } 19 | 20 | mix test || { echo 'Elixir tests failed!'; exit 1; } 21 | 22 | echo 'All tests succeeded!' 23 | exit 0 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 rdrop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExponentServerSdk.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :exponent_server_sdk, 7 | version: "0.2.0", 8 | elixir: "~> 1.7.2", 9 | name: "ExponentServerSdk", 10 | description: "Exponent Push Notification API library for Elixir", 11 | source_url: "https://github.com/rdrop/exponent-server-sdk-elixir", 12 | package: package(), 13 | docs: docs(), 14 | deps: deps() 15 | ] 16 | end 17 | 18 | def application do 19 | [applications: [:logger, :httpoison, :poison]] 20 | end 21 | 22 | defp deps do 23 | [ 24 | {:httpoison, ">= 1.2.0"}, 25 | {:poison, "~> 3.1.0"}, 26 | {:dialyze, "~> 0.2.1", only: [:dev, :test]}, 27 | {:credo, "~> 0.10.0", only: [:dev, :test]}, 28 | {:mock, "~> 0.3.2", only: :test}, 29 | {:ex_doc, ">= 0.0.0", only: [:dev, :test]}, 30 | {:inch_ex, ">= 0.0.0", only: [:dev, :test]} 31 | ] 32 | end 33 | 34 | def docs do 35 | [ 36 | readme: "README.md", 37 | main: ExponentServerSdk 38 | ] 39 | end 40 | 41 | defp package do 42 | [ 43 | maintainers: ["rdrop"], 44 | licenses: ["MIT"], 45 | links: %{ 46 | "Github" => "https://github.com/rdrop/exponent-server-sdk-elixir.git" 47 | } 48 | ] 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/exponent_server_sdk/parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExponentServerSdk.ParserTest do 2 | use ExUnit.Case 3 | 4 | import ExponentServerSdk.Parser 5 | 6 | doctest ExponentServerSdk.Parser 7 | 8 | test ".parse should decode a successful response into a named struct" do 9 | response = %{ 10 | body: 11 | "{ \"data\": {\"status\": \"ok\", \"id\": \"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\"} }", 12 | status_code: 200 13 | } 14 | 15 | expected = %{"status" => "ok", "id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"} 16 | assert {:ok, expected} == parse(response) 17 | end 18 | 19 | test ".parse should return an error when response is 400" do 20 | response = %{body: "{ \"errors\": \"Error message\" }", status_code: 400} 21 | assert {:error, "Error message", 400} == parse(response) 22 | end 23 | 24 | test ".parse_list should decode into a list of named structs" do 25 | json = """ 26 | {"data": 27 | [ 28 | {"status": "ok", "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}, 29 | {"status": "ok", "id": "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"} 30 | ] 31 | } 32 | """ 33 | 34 | response = %{body: json, status_code: 200} 35 | 36 | expected = [ 37 | %{"id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "status" => "ok"}, 38 | %{"id" => "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY", "status" => "ok"} 39 | ] 40 | 41 | assert {:ok, expected} == parse_list(response) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/exponent_server_sdk/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule ExponentServerSdk.Parser do 2 | @moduledoc """ 3 | A JSON parser tuned specifically for Expo Push Notification API responses. Based on Poison's 4 | excellent JSON decoder. 5 | """ 6 | 7 | @type http_status_code :: number 8 | @type success :: {:ok, map} 9 | @type success_list :: {:ok, [map]} 10 | @type error :: {:error, String.t(), http_status_code} 11 | 12 | @type parsed_response :: success | error 13 | @type parsed_list_response :: success_list | error 14 | 15 | @doc """ 16 | Parse a response expected to contain a single Map 17 | 18 | ## Examples 19 | 20 | It will parse into a map. with the message response status 21 | 22 | iex> response = %{body: "{\\"data\\": {\\"status\\": \\"ok\\", \\"id\\": \\"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\\"}}", status_code: 200} 23 | iex> return_value = ExponentServerSdk.Parser.parse(response) 24 | iex> return_value 25 | {:ok, %{"status" => "ok", "id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}} 26 | """ 27 | @spec parse(HTTPoison.Response.t()) :: success | error 28 | def parse(response) do 29 | handle_errors(response, fn body -> 30 | {:ok, json} = Poison.decode(body) 31 | json["data"] 32 | end) 33 | end 34 | 35 | @doc """ 36 | Parse a response expected to contain a list of Maps 37 | 38 | ## Examples 39 | 40 | It will parse into a list of maps with the message response status. 41 | 42 | iex> response = %{body: "{ \\"data\\": [{\\"status\\": \\"ok\\", \\"id\\": \\"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\\"}, {\\"status\\": \\"ok\\", \\"id\\": \\"YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY\\"}] }", status_code: 200} 43 | iex> return_value = ExponentServerSdk.Parser.parse_list(response) 44 | iex> return_value 45 | {:ok, [%{"id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "status" => "ok"}, %{"id" => "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY", "status" => "ok"}]} 46 | """ 47 | @spec parse_list(HTTPoison.Response.t()) :: success_list | error 48 | def parse_list(response) do 49 | handle_errors(response, fn body -> 50 | {:ok, json} = Poison.decode(body) 51 | json["data"] 52 | end) 53 | end 54 | 55 | # @spec handle_errors(response, ((String.t) -> any)) :: success | success_delete | error 56 | defp handle_errors(response, fun) do 57 | case response do 58 | %{body: body, status_code: status} when status in [200, 201] -> 59 | {:ok, fun.(body)} 60 | 61 | %{body: _, status_code: 204} -> 62 | :ok 63 | 64 | %{body: body, status_code: status} -> 65 | {:ok, json} = Poison.decode(body) 66 | {:error, json["errors"], status} 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/exponent_server_sdk/push_notification.ex: -------------------------------------------------------------------------------- 1 | defmodule ExponentServerSdk.PushNotification do 2 | @moduledoc """ 3 | Provides a basic HTTP interface to allow easy communication with the Exponent Push Notification 4 | API, by wrapping `HTTPotion`. 5 | 6 | ## Examples 7 | 8 | Requests are made to the Exponent Push Notification API by passing in a `Map` into one 9 | of the `Notification` module's functions. The correct URL to the resource is inferred 10 | from the module name. 11 | 12 | ExponentServerSdk.PushNotification.push(messages) 13 | {:ok, %{"status" => "ok", "id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}} 14 | 15 | Items are returned as instances of the given module's struct. For more 16 | details, see the documentation for each function. 17 | """ 18 | 19 | use HTTPoison.Base 20 | 21 | alias ExponentServerSdk.Parser 22 | alias ExponentServerSdk.PushMessage 23 | # Necessary for mocks in tests 24 | alias __MODULE__ 25 | 26 | @doc """ 27 | Send the push notification request when using a single message map 28 | """ 29 | @spec push(PushMessage.t()) :: Parser.success() | Parser.error() 30 | def push(message) when is_map(message) do 31 | message 32 | |> PushMessage.create() 33 | 34 | PushNotification.post!("send", message) 35 | |> Parser.parse() 36 | end 37 | 38 | @doc """ 39 | Send the push notification request when using a list of message maps 40 | """ 41 | @spec push_list(list(PushMessage.t())) :: Parser.success() | Parser.error() 42 | def push_list(messages) when is_list(messages) do 43 | messages 44 | |> PushMessage.create_from_list() 45 | 46 | PushNotification.post!("send", messages) 47 | |> Parser.parse_list() 48 | end 49 | 50 | @doc """ 51 | Send the get notification receipts request when using a list of ids 52 | """ 53 | @spec get_receipts(list()) :: Parser.success() | Parser.error() 54 | def get_receipts(ids) when is_list(ids) do 55 | ids 56 | |> PushMessage.create_receipt_id_list() 57 | 58 | PushNotification.post!("getReceipts", %{ids: ids}) 59 | |> Parser.parse() 60 | end 61 | 62 | @doc """ 63 | Automatically adds the correct url to each API request. 64 | """ 65 | @spec process_url(String.t()) :: String.t() 66 | def process_url(url) do 67 | "https://exp.host/--/api/v2/push/" <> url 68 | end 69 | 70 | @doc """ 71 | Automatically adds the correct headers to each API request. 72 | """ 73 | @spec process_request_headers(list) :: list 74 | def process_request_headers(headers \\ []) do 75 | headers 76 | |> Keyword.put(:Accepts, "application/json") 77 | |> Keyword.put(:"Accepts-Encoding", "gzip, deflate") 78 | |> Keyword.put(:"Content-Encoding", "gzip") 79 | |> Keyword.put(:"Content-Type", "application/json") 80 | end 81 | 82 | @doc """ 83 | Automatically process the request body using Poison JSON and GZip. 84 | """ 85 | def process_request_body(body) do 86 | body 87 | |> Poison.encode!() 88 | |> :zlib.gzip() 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/exponent_server_sdk/push_message_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExponentServerSdk.PushMessageTest do 2 | use ExUnit.Case 3 | 4 | import ExponentServerSdk.PushMessage 5 | 6 | alias ExponentServerSdk.PushMessage 7 | 8 | doctest ExponentServerSdk.PushMessage 9 | 10 | test ".create should encode the Map into the PushMessage struct" do 11 | message_map = %{ 12 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 13 | title: "Pushed!", 14 | body: "You got your first message" 15 | } 16 | 17 | message = %PushMessage{ 18 | badge: nil, 19 | body: "You got your first message", 20 | channelId: nil, 21 | data: nil, 22 | expiration: nil, 23 | priority: "default", 24 | sound: "default", 25 | title: "Pushed!", 26 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 27 | ttl: 0 28 | } 29 | 30 | assert message == create(message_map) 31 | end 32 | 33 | test ".create_from_list should encode the List of Maps into the PushMessage struct and return as chunked List of 100's" do 34 | message_list = [ 35 | %{ 36 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 37 | title: "Pushed!", 38 | body: "You got your first message" 39 | }, 40 | %{ 41 | to: "ExponentPushToken[YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY]", 42 | title: "Pushed Again!", 43 | body: "You got your second message" 44 | } 45 | ] 46 | 47 | messages = [ 48 | [ 49 | %PushMessage{ 50 | badge: nil, 51 | body: "You got your first message", 52 | channelId: nil, 53 | data: nil, 54 | expiration: nil, 55 | priority: "default", 56 | sound: "default", 57 | title: "Pushed!", 58 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 59 | ttl: 0 60 | }, 61 | %PushMessage{ 62 | badge: nil, 63 | body: "You got your second message", 64 | channelId: nil, 65 | data: nil, 66 | expiration: nil, 67 | priority: "default", 68 | sound: "default", 69 | title: "Pushed Again!", 70 | to: "ExponentPushToken[YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY]", 71 | ttl: 0 72 | } 73 | ] 74 | ] 75 | 76 | assert messages == create_from_list(message_list) 77 | end 78 | 79 | test ".create_receipt_id_list should return a validated list of ids into a map" do 80 | ids = ["XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"] 81 | 82 | assert ids == create_receipt_id_list(ids) 83 | end 84 | 85 | # test ".validate_push_token should ensure a valid token is provided" do 86 | # message = %PushMessage{ 87 | # badge: nil, 88 | # body: "You got your first message", 89 | # channelId: nil, 90 | # data: nil, 91 | # expiration: nil, 92 | # priority: "default", 93 | # sound: "default", 94 | # title: "Pushed!", 95 | # to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 96 | # ttl: 0 97 | # } 98 | # 99 | # assert true == validate_push_token(message) 100 | # end 101 | end 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ExponentServerSdk 2 | ======== 3 | [![Hex.pm](https://img.shields.io/hexpm/v/exponent_server_sdk.svg)](https://hex.pm/packages/exponent_server_sdk) 4 | [![Build Status](https://travis-ci.org/rdrop/exponent-server-sdk-elixir.svg?branch=master)](https://travis-ci.org/rdrop/exponent-server-sdk-elixir) 5 | [![Inline docs](https://inch-ci.org/github/rdrop/exponent-server-sdk-elixir.svg?branch=master)](https://inch-ci.org/github/rdrop/exponent-server-sdk-elixir) 6 | 7 | Use to send push notifications to Exponent Experiences from an Elixir/Phoenix server. 8 | 9 | ## Installation 10 | 11 | ExponentServerSdk is currently able to push single and multiple messages to the Expo Server and retrieve message delivery statuses from a list of IDs. 12 | 13 | All HTTPoison Post Request body are automatically GZIP compressed 14 | 15 | You can install it from Hex: 16 | 17 | ```elixir 18 | def deps do 19 | [{:exponent_server_sdk, "~> 0.2.0"}] 20 | end 21 | ``` 22 | 23 | Or from Github: 24 | 25 | ```elixir 26 | def deps do 27 | [{:exponent_server_sdk, github: "rdrop/exponent-server-sdk-elixir"}] 28 | end 29 | ``` 30 | 31 | and run `mix deps.get`. 32 | 33 | Now, list the `:exponent_server_sdk` application as your application dependency: 34 | 35 | ```elixir 36 | def application do 37 | [applications: [:exponent_server_sdk]] 38 | end 39 | ``` 40 | 41 | ## Usage 42 | 43 | ### Notifications 44 | 45 | The `ExponentServerSdk.PushNotification` is responsible for sending the messages and hits the latest version of the api. 46 | 47 | #### Single Message: 48 | 49 | ```elixir 50 | 51 | # Create a single message map 52 | message = %{ 53 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 54 | title: "Pushed!", 55 | body: "You got your first message" 56 | } 57 | 58 | # Send it to Expo 59 | {:ok, response} = ExponentServerSdk.PushNotification.push(message) 60 | 61 | # Example Response 62 | {:ok, %{"status" => "ok", "id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}} 63 | ``` 64 | 65 | #### Multiple Messages: 66 | ```elixir 67 | 68 | # Create a list of message maps (auto chunks list into lists of 100) 69 | message_list = [ 70 | %{ 71 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 72 | title: "Pushed!", 73 | body: "You got your first message" 74 | }, 75 | %{ 76 | to: "ExponentPushToken[YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY]", 77 | title: "Pushed Again!", 78 | body: "You got your second message" 79 | } 80 | ] 81 | 82 | # Send it to Expo 83 | {:ok, response} = ExponentServerSdk.PushNotification.push_list(messages) 84 | 85 | # Example Response 86 | {:ok,[ %{"status" => "ok", "id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}, %{"status" => "ok", "id" => "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"} ]} 87 | ``` 88 | 89 | #### Get Messages Delivery Statuses: 90 | ```elixir 91 | 92 | # Create a list of message ids 93 | ids = ["XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"] 94 | 95 | # Send it to Expo 96 | {:ok, response} = ExponentServerSdk.PushNotification.get_receipts(ids) 97 | 98 | # Example Response 99 | {:ok,[ %{ "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX": { "status": "ok" }, "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY": { "status": "ok" } } ]} 100 | ``` 101 | 102 | The complete format of the messages can be found [here.](https://docs.expo.io/versions/latest/guides/push-notifications#message-format) 103 | 104 | ## Contributing 105 | 106 | See the [CONTRIBUTING.md](CONTRIBUTING.md) file for contribution guidelines. 107 | 108 | ## License 109 | ExponentServerSdk is licensed under the MIT license. For more details, see the `LICENSE` 110 | file at the root of the repository. It depends on Elixir, which is under the 111 | Apache 2 license. 112 | 113 | ### Inspiration 114 | [ex_twilio](https://github.com/danielberkompas/ex_twilio) 115 | 116 | [hex]: http://hex.pm 117 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "credo": {:hex, :credo, "0.10.0", "66234a95effaf9067edb19fc5d0cd5c6b461ad841baac42467afed96c78e5e9e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "dialyze": {:hex, :dialyze, "0.2.1", "9fb71767f96649020d769db7cbd7290059daff23707d6e851e206b1fdfa92f9d", [:mix], [], "hexpm"}, 6 | "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, 7 | "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 11 | "inch_ex": {:hex, :inch_ex, "1.0.0", "18496a900ca4b7542a1ff1159e7f8be6c2012b74ca55ac70de5e805f14cdf939", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "inflex": {:hex, :inflex, "1.10.0", "8366a7696e70e1813aca102e61274addf85d99f4a072b2f9c7984054ea1b9d29", [:mix], [], "hexpm"}, 13 | "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 14 | "makeup": {:hex, :makeup, "0.5.1", "966c5c2296da272d42f1de178c1d135e432662eca795d6dc12e5e8787514edf7", [:mix], [{:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "makeup_elixir": {:hex, :makeup_elixir, "0.8.0", "1204a2f5b4f181775a0e456154830524cf2207cf4f9112215c05e0b76e4eca8b", [:mix], [{:makeup, "~> 0.5.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "meck": {:hex, :meck, "0.8.12", "1f7b1a9f5d12c511848fec26bbefd09a21e1432eadb8982d9a8aceb9891a3cf2", [:rebar3], [], "hexpm"}, 17 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 18 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 19 | "mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, 20 | "nimble_parsec": {:hex, :nimble_parsec, "0.2.2", "d526b23bdceb04c7ad15b33c57c4526bf5f50aaa70c7c141b4b4624555c68259", [:mix], [], "hexpm"}, 21 | "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, 22 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 23 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 24 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, 25 | } 26 | -------------------------------------------------------------------------------- /test/exponent_server_sdk/push_notification_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExponentServerSdk.PushNotificationTest do 2 | use ExUnit.Case, async: false 3 | 4 | import TestHelper 5 | 6 | alias ExponentServerSdk.PushMessage 7 | alias ExponentServerSdk.PushNotification 8 | 9 | doctest ExponentServerSdk.PushNotification 10 | 11 | test ".push should return the proper response from Expo" do 12 | message_map = %{ 13 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 14 | title: "Pushed!", 15 | body: "You got your first message" 16 | } 17 | 18 | response = %{data: %{"status" => "ok", "id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}} 19 | json = json_response(response, 200) 20 | 21 | with_fixture(:post!, json, fn -> 22 | # expected = {:ok, %{"status" => "ok", "id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}} 23 | expected = 24 | {:ok, 25 | %{ 26 | "status" => "error", 27 | "details" => %{"error" => "DeviceNotRegistered"}, 28 | "message" => 29 | "\"ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]\" is not a registered push notification recipient" 30 | }} 31 | 32 | assert expected == PushNotification.push(message_map) 33 | end) 34 | end 35 | 36 | test ".push_list should return the proper response from Expo" do 37 | message_list = [ 38 | %{ 39 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 40 | title: "Pushed!", 41 | body: "You got your first message" 42 | }, 43 | %{ 44 | to: "ExponentPushToken[YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY]", 45 | title: "Pushed Again!", 46 | body: "You got your second message" 47 | } 48 | ] 49 | 50 | response = %{ 51 | data: [ 52 | %{"status" => "ok", "id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}, 53 | %{"status" => "ok", "id" => "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"} 54 | ] 55 | } 56 | 57 | json = json_response(response, 200) 58 | 59 | with_fixture(:post!, json, fn -> 60 | # expected = { 61 | # :ok, 62 | # [ 63 | # %{"status" => "ok", "id" => "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}, 64 | # %{"status" => "ok", "id" => "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"} 65 | # ] 66 | # } 67 | expected = 68 | {:ok, 69 | [ 70 | %{ 71 | "status" => "error", 72 | "details" => %{"error" => "DeviceNotRegistered"}, 73 | "message" => 74 | "\"ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]\" is not a registered push notification recipient" 75 | }, 76 | %{ 77 | "status" => "error", 78 | "details" => %{"error" => "DeviceNotRegistered"}, 79 | "message" => 80 | "\"ExponentPushToken[YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY]\" is not a registered push notification recipient" 81 | } 82 | ]} 83 | 84 | assert expected == PushNotification.push_list(message_list) 85 | end) 86 | end 87 | 88 | test ".get_receipts should return the proper response from Expo" do 89 | ids = ["XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"] 90 | 91 | response = %{ 92 | data: %{ 93 | "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX": %{status: "ok"}, 94 | "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY": %{status: "ok"} 95 | } 96 | } 97 | 98 | json = json_response(response, 200) 99 | 100 | with_fixture(:post!, json, fn -> 101 | # expected = 102 | # {:ok, 103 | # %{ 104 | # "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" => %{"status" => "ok"}, 105 | # "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY" => %{"status" => "ok"} 106 | # }} 107 | expected = {:ok, %{}} 108 | 109 | assert expected == PushNotification.get_receipts(ids) 110 | end) 111 | end 112 | 113 | ### 114 | # HTTPotion API 115 | ### 116 | 117 | test ".process_request_headers adds the correct headers" do 118 | headers = PushNotification.process_request_headers([]) 119 | accepts = {:Accepts, "application/json"} 120 | accepts_encoding = {:"Accepts-Encoding", "gzip, deflate"} 121 | content_encoding = {:"Content-Encoding", "gzip"} 122 | content_type = {:"Content-Type", "application/json"} 123 | assert accepts in headers 124 | assert accepts_encoding in headers 125 | assert content_encoding in headers 126 | assert content_type in headers 127 | 128 | assert Keyword.keys(headers) == [ 129 | :"Content-Type", 130 | :"Content-Encoding", 131 | :"Accepts-Encoding", 132 | :Accepts 133 | ] 134 | end 135 | 136 | ### 137 | # Helpers 138 | ### 139 | 140 | def with_list_fixture(fun) do 141 | data = [ 142 | %{ 143 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 144 | title: "Pushed!", 145 | body: "You got your first message" 146 | }, 147 | %{ 148 | to: "ExponentPushToken[YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY]", 149 | title: "Pushed Again!", 150 | body: "You got your second message" 151 | } 152 | ] 153 | 154 | json = json_response(data, 200) 155 | 156 | with_fixture(:post!, json, fn -> 157 | expected = { 158 | :ok, 159 | [ 160 | [ 161 | %PushMessage{ 162 | badge: nil, 163 | body: "You got your first message", 164 | channelId: nil, 165 | data: nil, 166 | expiration: nil, 167 | priority: "default", 168 | sound: "default", 169 | title: "Pushed!", 170 | to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", 171 | ttl: 0 172 | }, 173 | %PushMessage{ 174 | badge: nil, 175 | body: "You got your second message", 176 | channelId: nil, 177 | data: nil, 178 | expiration: nil, 179 | priority: "default", 180 | sound: "default", 181 | title: "Pushed Again!", 182 | to: "ExponentPushToken[YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY]", 183 | ttl: 0 184 | } 185 | ] 186 | ] 187 | } 188 | 189 | fun.(expected) 190 | end) 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /lib/exponent_server_sdk/push_message.ex: -------------------------------------------------------------------------------- 1 | defmodule ExponentServerSdk.PushMessage do 2 | alias __MODULE__ 3 | 4 | @moduledoc """ 5 | Provides a basic payload structure to allow easy communication with the Exponent Push Notification. 6 | """ 7 | @enforce_keys [:to] 8 | defstruct to: nil, 9 | data: nil, 10 | title: nil, 11 | body: nil, 12 | ttl: 0, 13 | expiration: nil, 14 | priority: "default", 15 | sound: "default", 16 | badge: nil, 17 | channelId: nil 18 | 19 | @typedoc """ 20 | https://docs.expo.io/versions/v29.0.0/guides/push-notifications#message-format 21 | 22 | Based on the Expo Push Notification Message API JSON 23 | 24 | type PushMessage = { 25 | /** 26 | * An Expo push token specifying the recipient of this message. 27 | */ 28 | to: string, 29 | 30 | /** 31 | * A JSON object delivered to your app. It may be up to about 4KiB; the total 32 | * notification payload sent to Apple and Google must be at most 4KiB or else 33 | * you will get a "Message Too Big" error. 34 | */ 35 | data?: Object, 36 | 37 | /** 38 | * The title to display in the notification. Devices often display this in 39 | * bold above the notification body. Only the title might be displayed on 40 | * devices with smaller screens like Apple Watch. 41 | */ 42 | title?: string, 43 | 44 | /** 45 | * The message to display in the notification 46 | */ 47 | body?: string, 48 | 49 | /** 50 | * Time to Live: the number of seconds for which the message may be kept 51 | * around for redelivery if it hasn't been delivered yet. Defaults to 0. 52 | * 53 | * On Android, we make a best effort to deliver messages with zero TTL 54 | * immediately and do not throttle them 55 | * 56 | * This field takes precedence over `expiration` when both are specified. 57 | */ 58 | ttl?: number, 59 | 60 | /** 61 | * A timestamp since the UNIX epoch specifying when the message expires. This 62 | * has the same effect as the `ttl` field and is just an absolute timestamp 63 | * instead of a relative time. 64 | */ 65 | expiration?: number, 66 | 67 | /** 68 | * The delivery priority of the message. Specify "default" or omit this field 69 | * to use the default priority on each platform, which is "normal" on Android 70 | * and "high" on iOS. 71 | * 72 | * On Android, normal-priority messages won't open network connections on 73 | * sleeping devices and their delivery may be delayed to conserve the battery. 74 | * High-priority messages are delivered immediately if possible and may wake 75 | * sleeping devices to open network connections, consuming energy. 76 | * 77 | * On iOS, normal-priority messages are sent at a time that takes into account 78 | * power considerations for the device, and may be grouped and delivered in 79 | * bursts. They are throttled and may not be delivered by Apple. High-priority 80 | * messages are sent immediately. Normal priority corresponds to APNs priority 81 | * level 5 and high priority to 10. 82 | */ 83 | priority?: 'default' | 'normal' | 'high', 84 | 85 | // iOS-specific fields 86 | 87 | /** 88 | * A sound to play when the recipient receives this notification. Specify 89 | * "default" to play the device's default notification sound, or omit this 90 | * field to play no sound. 91 | * 92 | * Note that on apps that target Android 8.0+ (if using `exp build`, built 93 | * in June 2018 or later), this setting will have no effect on Android. 94 | * Instead, use `channelId` and a channel with the desired setting. 95 | */ 96 | sound?: 'default' | null, 97 | 98 | /** 99 | * Number to display in the badge on the app icon. Specify zero to clear the 100 | * badge. 101 | */ 102 | badge?: number, 103 | 104 | // Android-specific fields 105 | 106 | /** 107 | * ID of the Notification Channel through which to display this notification 108 | * on Android devices. If an ID is specified but the corresponding channel 109 | * does not exist on the device (i.e. has not yet been created by your app), 110 | * the notification will not be displayed to the user. 111 | * 112 | * If left null, a "Default" channel will be used, and Expo will create the 113 | * channel on the device if it does not yet exist. However, use caution, as 114 | * the "Default" channel is user-facing and you may not be able to fully 115 | * delete it. 116 | */ 117 | channelId?: string 118 | } 119 | """ 120 | @type t :: %PushMessage{ 121 | to: String.t(), 122 | data: map, 123 | title: String.t(), 124 | body: String.t(), 125 | ttl: integer, 126 | expiration: integer, 127 | priority: String.t(), 128 | sound: String.t() | nil, 129 | badge: integer, 130 | channelId: String.t() | nil 131 | } 132 | 133 | @doc """ 134 | Create a PushMessage struct from a single message map. 135 | 136 | ## Examples 137 | iex> message_map = %{to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", title: "Pushed!", body: "You got your first message"} 138 | iex> message = ExponentServerSdk.PushMessage.create(message_map) 139 | iex> message 140 | %ExponentServerSdk.PushMessage{badge: nil, body: "You got your first message", channelId: nil, data: nil, expiration: nil, priority: "default", sound: "default", title: "Pushed!", to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", ttl: 0} 141 | """ 142 | @spec create(map) :: PushMessage.t() 143 | def create(message) when is_map(message) do 144 | struct(PushMessage, message) 145 | end 146 | 147 | @doc """ 148 | Create a List of PushMessage structs from a list of maps chunked into lists of 100. 149 | 150 | ## Examples 151 | iex> message_list = [%{to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", title: "Pushed!", body: "You got your first message"}, %{to: "ExponentPushToken[YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY]", title: "Pushed Again!", body: "You got your second message"}] 152 | iex> messages = ExponentServerSdk.PushMessage.create_from_list(message_list) 153 | iex> messages 154 | [[%ExponentServerSdk.PushMessage{badge: nil, body: "You got your first message", channelId: nil, data: nil, expiration: nil, priority: "default", sound: "default", title: "Pushed!", to: "ExponentPushToken[XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX]", ttl: 0}, %ExponentServerSdk.PushMessage{ badge: nil, body: "You got your second message", channelId: nil, data: nil, expiration: nil, priority: "default", sound: "default", title: "Pushed Again!", to: "ExponentPushToken[YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY]", ttl: 0}]] 155 | """ 156 | @spec create_from_list(list(map)) :: list(PushMessage.t()) 157 | def create_from_list(messages) when is_list(messages) do 158 | messages 159 | |> Enum.map(fn msg -> struct(PushMessage, msg) end) 160 | |> Enum.chunk_every(100) 161 | 162 | # |> Enum.map(fn msg -> validate_push_token_from_message(struct(PushMessage, msg)) end) 163 | # |> Enum.reject(fn msg -> msg == nil end) 164 | end 165 | 166 | @doc """ 167 | Create a List of PushMessageIds from a list. 168 | 169 | ## Examples 170 | iex> ids = ["XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"] 171 | iex> ids = ExponentServerSdk.PushMessage.create_receipt_id_list(ids) 172 | iex> ids 173 | ["XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"] 174 | """ 175 | @spec create_receipt_id_list(list) :: list 176 | def create_receipt_id_list(push_token_ids) when is_list(push_token_ids) do 177 | push_token_ids 178 | 179 | # |> Enum.map(fn push_token_id -> validate_push_token_id_for_receipts(push_token_id) end) 180 | # |> Enum.reject(fn push_token_id -> push_token_id == nil end) 181 | end 182 | 183 | # @spec validate_push_token_id_for_receipts(String.t()) :: String.t() | nil 184 | # defp validate_push_token_id_for_receipts(push_token_id) when is_binary(push_token_id) do 185 | # if Regex.match?( 186 | # ~r/^[a-z\d]{8}-[a-z\d]{4}-[a-z\d]{4}-[a-z\d]{4}-[a-z\d]{12}$/i, 187 | # push_token_id 188 | # ) do 189 | # push_token_id 190 | # else 191 | # nil 192 | # end 193 | # end 194 | 195 | # @spec validate_push_token_from_message(PushMessage.t()) :: PushMessage.t() | nil 196 | # defp validate_push_token_from_message(%PushMessage{to: push_token} = message) 197 | # when is_map(message) do 198 | # token_list = 199 | # Regex.scan(~r/(?<=^ExponentPushToken\[)(.*)(?=[$\]])/, push_token, capture: :first) 200 | # 201 | # [raw_token] = List.flatten(token_list) 202 | # 203 | # if Regex.match?(~r/^ExponentPushToken\[/, push_token) && Regex.match?(~r/\]$/, push_token) && 204 | # Regex.match?(~r/^[a-z\d]{8}-[a-z\d]{4}-[a-z\d]{4}-[a-z\d]{4}-[a-z\d]{12}$/i, raw_token) do 205 | # message 206 | # else 207 | # nil 208 | # end 209 | # end 210 | end 211 | --------------------------------------------------------------------------------