{"id":20763780,"url":"https://github.com/revenuecat/purchases-roku","last_synced_at":"2026-02-25T16:03:37.859Z","repository":{"id":256625455,"uuid":"780468282","full_name":"RevenueCat/purchases-roku","owner":"RevenueCat","description":"Roku in-app purchases and subscriptions made easy.","archived":false,"fork":false,"pushed_at":"2025-09-04T10:30:52.000Z","size":473,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":22,"default_branch":"main","last_synced_at":"2025-09-04T12:29:45.932Z","etag":null,"topics":["brightscript","roku"],"latest_commit_sha":null,"homepage":"https://www.revenuecat.com","language":"Brightscript","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/RevenueCat.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"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,"zenodo":null}},"created_at":"2024-04-01T14:48:29.000Z","updated_at":"2025-09-04T10:30:54.000Z","dependencies_parsed_at":null,"dependency_job_id":"5711b7c9-4db5-4dc8-be1c-59d8752d61ac","html_url":"https://github.com/RevenueCat/purchases-roku","commit_stats":null,"previous_names":["revenuecat/purchases-roku"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/RevenueCat/purchases-roku","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RevenueCat%2Fpurchases-roku","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RevenueCat%2Fpurchases-roku/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RevenueCat%2Fpurchases-roku/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RevenueCat%2Fpurchases-roku/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RevenueCat","download_url":"https://codeload.github.com/RevenueCat/purchases-roku/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RevenueCat%2Fpurchases-roku/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29829410,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-25T15:41:19.027Z","status":"ssl_error","status_checked_at":"2026-02-25T15:40:47.150Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["brightscript","roku"],"created_at":"2024-11-17T10:47:31.773Z","updated_at":"2026-02-25T16:03:37.854Z","avatar_url":"https://github.com/RevenueCat.png","language":"Brightscript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Welcome to the RevenueCat Roku SDK.\n\nRevenueCat's Roku support is in its early stages. All feedback and issue reports are welcomed.\n\nCheck out our latest documentation on [how to configure products in Roku](https://www.revenuecat.com/docs/getting-started/entitlements/roku-products) and [how to use the Roku SDK](https://www.revenuecat.com/docs/getting-started/installation/roku).\n\n# How to setup your Roku developer account\n\nFollow the [First Steps](https://developer.roku.com/en-gb/docs/developer-program/getting-started/first-steps.md) guide to create a Roku developer account, login to your Roku device and enable developer mode on your Roku device.\n\n# How to setup a channel\n\nOnce you have your developer account created, head to the [dashboard](https://developer.roku.com/dev/dashboard)\n\n1. First, [create a new Beta Channel](https://developer.roku.com/en-gb/docs/developer-program/publishing/channel-publishing-guide.md#create-a-channel).\n2. Make sure the  Beta Channel is [enabled for billing testing](https://developer.roku.com/en-gb/docs/developer-program/roku-pay/testing/billing-testing.md).\n3. Under \"Monetization\" -\u003e \"Test users\", [add a test user](https://developer.roku.com/en-gb/docs/developer-program/roku-pay/quickstart/test-users.md) with the email associated to your Roku device. .\n4. Under \"Monetization\" -\u003e \"Product\", follow the process to submit the tax documents, and after you're approved, [create the products](https://developer.roku.com/en-gb/docs/developer-program/roku-pay/quickstart/in-channel-products.md#adding-a-new-product).\n\nIMPORTANT: Only the \"root account user\" can test billing on device. If you get added as a collaborator to someone else's developer account, billing testing will not work. You'll need to create your own developer account.\n\n# How to configure a Roku app in RevenueCat\n\n1. Make sure your project has been enabled to create Roku apps. If you're not sure, talk to your RevenueCat contact.\n2. Open the RevenueCat dashboard, select your project, and click on \"Add app\".\n3. Select \"Roku Store\", enter your app's name and the Roku Pay API key you can find in https://developer.roku.com/rpay-web-services\n4. Once the app is created, click on the \"Roku Server to Server notifications settings\", copy the \"Roku Push Notification URL\" which starts with `https://api.revenuecat.com/v1/incoming-webhooks/roku-pay-jwt-notification` and paste it in the Push notifications URL section on the same page where you found the Roku Pay API key.\n5. Click on \"Public API Key\" and copy over the value which should start with \"roku_XXXXXX\". You will need it to configure the SDK later.\n\n# How to install the SDK\n\n1. Clone the repository:\n\n```bash\ngit clone https://github.com/RevenueCat/purchases-roku`\n```\n\n2. Copy the `components/purchases` folder into your app's `components` folder.\n3. Copy the `source/Purchases.brs` file into your app's `source` folder.\n4. Import the SDK in the .xml file of the component where you want to use it:\n\n```xml\n\u003c!-- Importing the RevenueCat SDK --\u003e\n\u003cscript type=\"text/brightscript\" uri=\"pkg:/source/Purchases.brs\" /\u003e\n```\n\n# How to use the SDK\n\n**Important:** The SDK should be used only from SceneGraph components. Calling it from the main thread or from a Task component is not supported.\n\n## Configuring the SDK\n\nInitialize the SDK with your api key. You typically do this inside the `init()` method of your main scene.\n\n```brightscript\n  sub init()\n    Purchases().configure({\n      \"apiKey\": \"roku_XXXXX\",\n      \"userId\": \"my_user_id\" ' optional, will use an anonymous user id if not provided\n    })\n  end sub\n```\n\n## Callbacks and error handling\n\nIn methods of the SDK which perform async operations, you can get the result by passing a sub routine or a callback name.\n\n- The first parameter will contain the result.\n- The second parameter will contain an error if there was one, or `invalid` if there wasnt.\n\n**Example:**\n\n```brightscript\nsub init()\n  Purchases().logIn(my_user_id, sub(subscriber, error)\n    if error \u003c\u003e invalid\n      print \"there was en error\"\n    else\n      print subscriber\n    end if\n  end sub)\n\n  ' To use a function as callback, pass its name as second parameter\n  Purchases().logIn(my_user_id, \"onSubscriberReceived\")\nend sub\n\nsub onSubscriberReceived(e as object)\n    data = e.GetData()\n    if data.error \u003c\u003e invalid\n      print \"there was en error\"\n    else\n      print data.result\n    end if\nend sub\n```\n\n## Models\n\n### Subscriber\n\nThe subscriber object is returned from different APIs. Here's an example of what it looks like:\n\n```brightscript\n{\n  activeSubscriptions: [\"my_product_id\"]\n  allExpirationDatesByProduct: {\n    \"my_product_id\": \u003cComponent: roDateTime\u003e\n  }\n  allPurchaseDatesByProduct: {\n    \"my_product_id\": \u003cComponent: roDateTime\u003e\n  }\n  allPurchasedProductIds: [\"my_product_id\"]\n  entitlements: {\n    all: {\n      billingIssueDetectedAt: invalid\n      expirationDate: \u003cComponent: roDateTime\u003e\n      identifier: \"premium\"\n      isActive: false\n      isSandbox: true\n      latestPurchaseDate: \u003cComponent: roDateTime\u003e\n      originalPurchaseDate: \u003cComponent: roDateTime\u003e\n      ownershipType: \"PURCHASED\"\n      periodType: \"normal\"\n      productIdentifier: \"my_product_identifier\"\n      productPlanIdentifier: invalid\n      store: \"app_store\"\n      unsubscribeDetectedAt: invalid\n      willRenew: false\n    }\n    active: {}\n  }\n  firstSeen: \u003cComponent: roDateTime\u003e\n  lastSeen: \u003cComponent: roDateTime\u003e\n  latestExpirationDate: \u003cComponent: roDateTime\u003e\n  managementUrl: invalid\n  nonSubscriptionTransactions: [\n    {\n        isSandbox: false\n        originalPurchaseDate: \u003cComponent: roDateTime\u003e\n        purchaseDate: \u003cComponent: roDateTime\u003e\n        store: \"roku\"\n        storeTransactionIdentifier: \"XXXXXXX\"\n        transactionIdentifier: \"XXXXXXX\"\n        productIdentifier: \"my_product_id\"\n    }\n  ]\n  originalAppUserId: \"$RCAnonymousID:XXXXXXXXXXXXXXXX\"\n  originalApplicationVersion: \"1.0\"\n  originalPurchaseDate: \u003cComponent: roDateTime\u003e\n  requestDate: \u003cComponent: roDateTime\u003e\n}\n```\n\n### Error\n\nThe error model constains two fields: `code` and `message`\n\n```\n{\n  code: 1234,\n  message: \"There as an error\",\n}\n```\n\n## Making a purchase\n\nAs a parameter to the `purchase()` method, you can pass an associative array containing one of the following values:\n\n- `code`: A string containing the product id.\n- `product`: From the `getOfferings` result: e.g. `offerings.current.annual.storeProduct`\n- `package`: From the `getOfferings` result: e.g. `offerings.current.annual`\n\nAdditionally,you can pass the following optional parameters:\n\n- `action`: To perform a product change. Valid values: `Upgrade` or `Downgrade`\n\n```brightscript\nPurchases().purchase({ code: \"product_id\" }, sub(result, error)\n  ' error will be present if the transaction could not be finished\n  if error \u003c\u003e invalid\n    if result \u003c\u003e invalid and result.userCancelled = true\n      print \"The user cancelled the purchase\"\n    end if\n    print \"The purchase could not be completed\"\n  else\n    ' The raw transaction generated by the purchase\n    result.transaction\n\n    ' {\n    '     amount: \"$0.00\"\n    '     code: \"yearly_subscription_product\"\n    '     description: \"Yearly Subscription\"\n    '     externalCode: \"\"\n    '     freeTrialQuantity: 0\n    '     freeTrialType: \"None\"\n    '     name: \"Yearly Subscription\"\n    '     originalAmount: \"0\"\n    '     productType: \"YearlySub\"\n    '     promotionApplied: false\n    '     purchaseId: \"00000000-0000-0000-0000-000000000000\"\n    '     qty: 1\n    '     replacedOffers: []\n    '     replacedSubscriptionId: \"\"\n    '     rokuCustomerId: \"00000000-0000-0000-0000-000000000000\"\n    '     total: \"$0.00\"\n    '     trialCost: \"$0.99\"\n    '     trialQuantity: 1\n    '     trialType: \"Years\"\n    ' }\n\n    ' The subscriber object\n    result.subscriber\n  end\nend sub)\n```\n\n## Getting offerigns\n\n```brightscript\nPurchases().getOfferings(sub(offerings, error)\n  if error \u003c\u003e invalid\n    print \"There was an error fetching offerings\n  else\n    ' The offerings object\n\n    ' {\n    '   current: {\n    '     identifier: \"my_id\",\n    '     metadata: { }, ' Metadata set in the Offering configuration\n    '     description: \"Offering description\",\n    '     annual: {}, ' The configured Annual package, if available\n    '     monthly: {}, ' The configured Monthly package, if available\n    '     ' A list of all available packages\n    '     availablePackages: [\n    '       {\n    '         identifier: \"package_identifier\",\n    '         packageType: \"custom\",\n    '         ' The raw Roku store product\n    '         storeProduct: {\n    '           code: \"yearly_subscription_product\"\n    '           cost: \"$1.99\"\n    '           description: \"Yearly Subscription\"\n    '           freeTrialQuantity: 0\n    '           freeTrialType: \"None\"\n    '           HDPosterUrl: \"\"\n    '           id: \"00000000-0000-0000-0000-000000000000\"\n    '           inStock: \"true\"\n    '           name: \"Yearly Subscription\"\n    '           offerEndDate: \"\"\n    '           offerStartDate: \"\"\n    '           productImagePortrait: \"\"\n    '           productImageUrl: \"\"\n    '           productType: \"YearlySub\"\n    '           qty: 0\n    '           SDPosterUrl: \"\"\n    '           trialCost: \"$0.99\"\n    '           trialQuantity: 12\n    '           trialType: \"Months\"\n    '         }\n    '       }\n    '     ],\n    '   },\n    '   all: {\n    '     ' An associative array of all the offerings, keyed by their identifier\n    '   }\n    ' }\n  end if\nend sub)\n```\n\nYou can also retrieve the current offering for a placement identifier. Use this to access offerings defined by targeting placements configured in the RevenueCat dashboard:\n\n```brightscript\nPurchases().getOfferings(sub(offerings, error)\n  if error \u003c\u003e invalid\n    print \"There was an error fetching offerings\n  else\n    my_offering = Purchases().currentOfferingForPlacement({offerings: offerings, placementId: \"my_placement\" })\n  end if\nend sub)\n```\n\n## Subscriber logIn and logOut\n\n```brightscript\n' subscriber: The new user subscriber info\n' error: Will be present if there was an error during the process\nPurchases().logIn(\"my_user_id\", sub(subscriber, error)\nend sub)\n\n' Calling logOut generated a new anonymous user\n' subscriber: The new anonymous user subscriber info\n' error: Will be present if there was an error during the process\nPurchases().logOut(sub(subscriber, error)\nend sub)\n```\n\n## Getting the current App User ID, and checking if the current user is anonymous\n\n' Both callback and synchronous methods are available.\n\n```brightscript\n' isAnonymous: boolean indicating whether the current user is anonymous\nPurchases().isAnonymous(sub(isAnonymous, error)\nend sub)\nisAnonymous = Purchases().isAnonymous()\n\n' appUserId: string representing the current user ID, can be anonymous\nPurchases().appUserId(sub(appUserId, error)\nend sub)\nappUserId = Purchases().appUserId()\n```\n\n## Getting customer info\n\n```brightscript\n' subscriber: The current subscriber info\n' error: Will be present if there was an error during the process\nPurchases().getCustomerInfo(sub(subscriber, error)\nend sub)\n```\n\n## Set subscriber attributes\n\n```brightscript\n' success: Will be true if the attributes were successfully synchronized\n' error: Will be present if there was an error during the process\nPurchases().setAttributes({ \"my attribute\": \"my value\" }, sub(success, error)\nend sub)\n```\n\n## Sync Purchases\n\nThis method will post all purchases associated with the current Roku account to RevenueCat and become associated with the current User ID.\nIt should only be used if you're migrating from using your own Roku Pay implementation and want to track previous purchases in RevenueCat\n\n```brightscript\n' subscriber: The current subscriber info\n' error: Will be present if there was an error during the process\nPurchases().syncPurchases(sub(subscriber, error)\nend sub)\n```\n\n## Tying everything together\n\nFor most apps, the usage of the SDK would look like this:\n\n1. Initialise the SDK\n2. Log in the user\n4. Check if the entitlement is active\n5. Fetch offerings and show your paywall UI\n6. Make a purchase\n\n```brightscript\nsub init()\n  ' Initialize the SDK\n  Purchases().configure({\n      \"apiKey\": \"roku_XXXXX\",\n      \"userId\": \"my_user_id\" ' optional, will use an anonymous user id if not provided\n  })\n  ' Login the user\n  Purchases().logIn(m.my_user_id, sub(subscriber, error)\n      if error = invalid\n        ' If my entitlement is not active, fetch offerings to show the paywall\n        if subscriber.entitlements.my_entitlement.isActive = false\n          fetchOfferings()\n        end if\n      end if\n  end sub)\nend sub\n\nsub fetchOfferings()\n  Purchases().getOfferings(sub(offerings, error)\n    if error = invalid\n      ' Use offerings to build your paywall UI.\n      ' Then call purchaseProduct with the one selected by the user\n      purchaseProduct(offerings.current.annual)\n    end if\n  end sub)\nend sub\n\n' Call purchaseProduct when the user decides to initiate a purchase\nsub purchaseProduct(product)\n  Purchases().purchase(product, sub(result, error)\n    if error = invalid\n      print \"Purchase successful\"\n      print result.transaction\n      print result.subscriber\n    end if\n  end sub)\nend sub\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frevenuecat%2Fpurchases-roku","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frevenuecat%2Fpurchases-roku","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frevenuecat%2Fpurchases-roku/lists"}