{"id":17472461,"url":"https://github.com/sleeplessbyte/expo-server-sdk-ruby","last_synced_at":"2025-09-22T04:31:41.460Z","repository":{"id":56845238,"uuid":"420810391","full_name":"SleeplessByte/expo-server-sdk-ruby","owner":"SleeplessByte","description":null,"archived":false,"fork":false,"pushed_at":"2024-08-26T14:57:20.000Z","size":74,"stargazers_count":7,"open_issues_count":2,"forks_count":6,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-18T19:46:39.238Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SleeplessByte.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-10-24T22:26:38.000Z","updated_at":"2024-08-30T20:17:56.000Z","dependencies_parsed_at":"2024-10-29T01:16:55.289Z","dependency_job_id":"2224ea11-1f43-4ca8-8cb5-efeb36067b0b","html_url":"https://github.com/SleeplessByte/expo-server-sdk-ruby","commit_stats":{"total_commits":23,"total_committers":5,"mean_commits":4.6,"dds":"0.26086956521739135","last_synced_commit":"d19b16d44e93b0bdf6af79803503ba606c08b225"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SleeplessByte%2Fexpo-server-sdk-ruby","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SleeplessByte%2Fexpo-server-sdk-ruby/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SleeplessByte%2Fexpo-server-sdk-ruby/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SleeplessByte%2Fexpo-server-sdk-ruby/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SleeplessByte","download_url":"https://codeload.github.com/SleeplessByte/expo-server-sdk-ruby/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":233824409,"owners_count":18736001,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-18T17:18:06.252Z","updated_at":"2025-09-22T04:31:36.153Z","avatar_url":"https://github.com/SleeplessByte.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Expo::Server::SDK\n\n[![Build status](https://github.com/SleeplessByte/expo-server-sdk-ruby/actions/workflows/main.yml/badge.svg)](https://github.com/SleeplessByte/expo-server-sdk-ruby/actions/workflows/main.yml) [![Gem version](https://img.shields.io/gem/v/expo-server-sdk?label=gem)](https://rubygems.org/gems/expo-server-sdk)\n\nThis gem was written because of the relatively little attention and improvement [expo-server-sdk-ruby](https://github.com/expo-community/expo-server-sdk-ruby) receives.\n\nIt does **not** work in the same way, so you'll want to read the documentation carefully if you intend to migrate.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'expo-server-sdk'\n```\n\nAnd then execute:\n\n```shell\nbundle install\n```\n\nOr install it yourself as:\n\n```shell\ngem install expo-server-sdk\n```\n\n## Usage\n\n```ruby\n# Not necessary in Rails. Zeitwerk will require this correctly for you.\nrequire 'expo/server/sdk'\n\n# Create a new Expo SDK client optionally providing an access token if you\n# have enabled push security\nclient = Expo::Push::Client.new(access_token: '\u003caccess-token\u003e');\n\n# If you do not have an access token, you can call it like this:\n# client = Expo::Push::Client.new\n\n# Create the messages that you want to send to clients\nmessages = [];\n\nsome_push_tokens.each do |push_token|\n  # Each push token looks like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]\n\n  # Check that all your push tokens appear to be valid Expo push tokens.\n  # If you don't do this, this library will raise an error when trying to\n  # create the notification.\n  #\n  unless Expo::Push.expo_push_token?(push_token)\n    puts \"Push token #{pushToken} is not a valid Expo push token\"\n    next\n  end\n\n  # Construct a message (see https://docs.expo.io/push-notifications/sending-notifications/)\n  #\n  # Use client.notification, Expo::Push::Notification.new,\n  # or Expo::Push::Notification.to, then follow it with one or more chainable\n  # API calls, including, but not limited to:\n  #\n  # - #to: add recipient (or #add_recipient),\n  #        add recipients (or #add_recipients)\n  # - #title\n  # - #subtitle\n  # - #body (or #content)\n  # - #data\n  # - #priority\n  # - #sound\n  # - #channel_id\n  # - #category_id\n  #\n  messages \u003c\u003c client.notification\n    .to(push_token)\n    .sound('default')\n    .body('This is a test notification')\n    .data({ withSome: 'data' })\nend\n\n# The Expo push notification service accepts batches of notifications so that\n# you don't need to send 1000 requests to send 1000 notifications. We\n# recommend you batch your notifications to reduce the number of requests and\n# to compress them (notifications with similar content will get compressed).\n#\n# Using #send or #send! will automatically batch your messages.\n#\n# When using #send, the result is an array of tickets per batched chunk, or may\n# be an error, such as a TicketsWithErrors error. It's up to you to inspect and\n# handle those errors.\n#\n# When using #send!, all batches will first execute, and then the first error\n# received is raised.\n#\ntickets = client.send!(messages)\n\n# You can #explain(error) to attempt to explain nested errors. For example, say\n# a batch contains failed errors, or completely failed pages:\n#\ntickets.each_error do |error|\n  if error.respond_to?(:explain)\n    puts error.explain\n    # =\u003e \"The device cannot receive push notifications anymore and you should\n    #     stop sending messages to the corresponding Expo push token.\"\n\n    puts error.message\n    # =\u003e \"\"ExpoPushToken[xxxxxxxxxxxxxxxxxxxxxx]\" is not a registered push\n    #     notification recipient\"\n    #\n    # In the case of an DeviceNotRegistered, you can attempt to extract the\n    # faulty push token:\n\n    error.original_push_token\n    # =\u003e ExpoPushToken[xxxxxxxxxxxxxxxxxxxxxx]\n  else\n    puts error.message\n    # =\u003e \"This indicates the entire request had an error\"\n  end\nend\n\n# Later, after the Expo push notification service has delivered the\n# notifications to Apple or Google (usually quickly, but allow the the service\n# up to 30 minutes when under load), a \"receipt\" for each notification is\n# created. The receipts will be available for at least a day; stale receipts\n# are deleted.\n#\n# The ID of each receipt is sent back in the response \"ticket\" for each\n# notification. In summary, sending a notification produces a ticket, which\n# contains a receipt ID you later use to get the receipt.\n#\n# The receipts may contain error codes to which you must respond. In\n# particular, Apple or Google may block apps that continue to send\n# notifications to devices that have blocked notifications or have uninstalled\n# your app. Expo does not control this policy and sends back the feedback from\n# Apple and Google so you can handle it appropriately.\n#\n# Note: this will silently skip over any errors encountered. Use #each_error\n#       to attempt to handle them yourself.\nreceipt_ids = tickets.ids\n\n# You may want to be doing this in some job context, so this gem doesn't batch\n# and call the endpoint manually, but you can generate the batches, and send\n# then individually:\nbatches = tickets.batch_ids\n\n# Now you can schedule your jobs, thread, or run this inline. All would work.\nbatches.each do |receipt_ids|\n  # \u003c\u003c schedule a job with this batch of ids \u003e\u003e\n  # ...\n  # inside the job or inline\n  receipts = client.receipts(receipt_ids)\n\n  # You can #explain(error) to attempt to explain receipts that have an\n  # error status.\n  #\n  receipts.each_error do |receipt|\n    puts error.explain\n    # =\u003e \"The device cannot receive push notifications anymore and you should\n    #     stop sending messages to the corresponding Expo push token.\"\n\n    puts error.message\n    # =\u003e \"\"ExpoPushToken[xxxxxxxxxxxxxxxxxxxxxx]\" is not a registered push\n    #     notification recipient\"\n    #\n    # In the case of an DeviceNotRegistered, you can attempt to extract the\n    # faulty push token:\n\n    error.original_push_token\n    # =\u003e ExpoPushToken[xxxxxxxxxxxxxxxxxxxxxx]\n  end\n\n  # Because not all receipts may be returned, it is imported to schedule, or\n  # retry the unresolved receipts at a later point in time:\n  unresolved_ids = receipts.unresolved_ids\n\n  # ...\n  receipts = client.receipts(unresolved_ids) if unresolved_ids.length \u003e 0\nend\n```\n\n### Logging\n\nIt is very likely that you'll want to develop with logging turned on.\nThis can be accomplished by passing in a logger instance:\n\n```ruby\nrequire 'logger'\n\nlogger = Logger.new(STDOUT);\nclient = Expo::Push::Client.new(logger: logger)\n\n# Now when doing requests like so:\nclient.send(notification)\n\n# ...it will log\n#\n# I, [2021-10-25T02:16:11.284901 #16448]  INFO -- : \u003e POST https://exp.host/--/api/v2/push/send\n# D, [2021-10-25T02:16:11.285601 #16448] DEBUG -- : Accept: application/json\n# Accept-Encoding: gzip\n# User-Agent: expo-server-sdk-ruby/0.1.0\n# Connection: Keep-Alive\n# Content-Type: application/json; charset=UTF-8\n# Host: exp.host\n#\n# [{\"to\":[\"ExpoPushToken[xxxxxxxxxxxxxxxxxxxxxx]\"]}]\n```\n\nFor more advanced logging, or instrumentation in general, use the Instrumentation feature.\nIt expects an `ActiveSupport::Notifications`-compatible instrumenter.\n\nThese are available in most Rails projects by default.\n\n```ruby\nActiveSupport::Notifications.subscribe('start_request.http') do |name, start, finish, id, payload|\n  pp :name =\u003e name, :start =\u003e start.to_f, :finish =\u003e finish.to_f, :id =\u003e id, :payload =\u003e payload\nend\n\nclient = Expo::Push::Client.new(instrumentation: true)\n\n# Now when doing requests like so:\nclient.send(notification)\n\n# ...it will instrument\n# =\u003e {name: .., start: .., finish: .., id: .., payload: ..}\n```\n\nYou can configure the namespace (and instrumentation):\n\n```ruby\nclient = Expo::Push::Client.new(\n  instrumentation: {\n    instrumenter: ActiveSupport::Notifications.instrumenter,\n    namespace: \"my_http\"\n  }\n)\n```\n\n### Example of error handling\n\nHere is an example of error handling when using Rails, given a Rails model called `PushNotificationToken`.\n\nThe most important thing is that you remove push tokens that are invalid, you fix push tokens that don't have the right experience ID and you stop sending push notifications if you're not allowed (e.g. the device is no longer registered).\n\n```ruby\n# Remove invalid push notification tokens, and remove tokens that failed\n# and contain a token (DeviceNotRegistered)\ntickets.each_error do |error|\n\n  if error.is_a?(Expo::Push::PushTokenInvalid)\n    # Destroy the tokens that match because they are not valid\n    PushNotificationToken.where(push_token: error.token).destroy_all\n  \n  elsif error.is_a?(Expo::Push::TicketsWithErrors)\n    retryable = true\n\n    error.errors.each do |error_data|\n      \n      # This block tries to fix the token experiences, and then reschedules\n      # the job. When it fixes tokens, it notifies bugsnag, so we know that\n      # this happened. If it keeps happening, there is a bug in the query\n      # or registration code.\n      if error_data['code'] == \"PUSH_TOO_MANY_EXPERIENCE_IDS\"\n      \n        # Go through all the details\n        error_data['details'].each do |correct_experience, tokens|\n        \n          # Find the incorrect instances\n          instances = PushNotificationToken\n            .where.not(experience_id: correct_experience)\n            .where(push_token: tokens)\n\n          next if instances.blank?\n          next unless instances.update_all(experience_id: correct_experience)\n\n          instances.each do |instance|\n            Bugsnag.notify(\n              StandardError.new(\n                format(\n                  'When trying to push, a push token (token: %s) had the wrong experience id (old: %s). ' \\\n                  'It has been updated (%s).',\n                  instance.push_token,\n                  instance.experience_id_was,\n                  instance.experience_id\n                )\n              )\n            )\n          end\n        end\n        \n      # If there is a different error, report to our error tracker\n      else\n        retryable = false\n        # Otherwise, notify as actual error.\n        Bugsnag.notify(error_data)\n      end\n    end\n      \n    if retries \u003e 10\n      return Bugsnag.notify(\n        StandardError.new(\n          'Not sending push notification because it was retried \u003e 10 times.'\n        )\n      )\n    end\n\n    # If the error is not a fatal one, the push can be retried. This helps\n    # with making sure you always send the push notification(s) even when\n    # the service intermittendly fails.\n    if retryable\n      ScheduledPushNotificationJob\n        .set(wait: 1.minute * (retries + 1))\n        .perform_later(\n          notification: notification,\n          event: event,\n          updated_at: updated_at,\n          retries: retries + 1\n        )\n    end\n      \n  # Otherwise it's an explanable error\n  elsif error.respond_to?(:explain)\n      \n    # If the error contains a token it always needs to be removed\n    original_token = error.original_push_token\n    next unless original_token\n\n    PushNotificationToken.where(push_token: original_token).destroy_all\n  else\n    \n    # Notify us of any other type of error\n    Bugsnag.notify(error)\n  end\nend\n\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies.\nThen, run `rake test` to run the tests.\nYou can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`.\nTo release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at \u003chttps://github.com/SleeplessByte/expo-server-sdk-ruby\u003e.\nThis project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/SleeplessByte/expo-server-sdk-ruby/blob/main/CODE_OF_CONDUCT.md).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the `Expo::Server::SDK` project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/SleeplessByte/expo-server-sdk/blob/main/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsleeplessbyte%2Fexpo-server-sdk-ruby","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsleeplessbyte%2Fexpo-server-sdk-ruby","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsleeplessbyte%2Fexpo-server-sdk-ruby/lists"}