Welcome to the RevenueCat Roku SDK.
RevenueCat's Roku support is in its early stages. All feedback and issue reports are welcomed.
Check out our latest documentation on how to configure products in Roku and how to use the Roku SDK.
Follow the First Steps guide to create a Roku developer account, login to your Roku device and enable developer mode on your Roku device.
Once you have your developer account created, head to the dashboard
- First, create a new Beta Channel.
- Make sure the Beta Channel is enabled for billing testing.
- Under "Monetization" -> "Test users", add a test user with the email associated to your Roku device. .
- Under "Monetization" -> "Product", follow the process to submit the tax documents, and after you're approved, create the products.
IMPORTANT: 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.
- Make sure your project has been enabled to create Roku apps. If you're not sure, talk to your RevenueCat contact.
- Open the RevenueCat dashboard, select your project, and click on "Add app".
- 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
- 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. - 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.
- Clone the repository:
git clone https://github.com/RevenueCat/purchases-roku`
- Copy the
components/purchases
folder into your app'scomponents
folder. - Copy the
source/Purchases.brs
file into your app'ssource
folder. - Import the SDK in the .xml file of the component where you want to use it:
<!-- Importing the RevenueCat SDK -->
<script type="text/brightscript" uri="pkg:/source/Purchases.brs" />
Important: The SDK should be used only from SceneGraph components. Calling it from the main thread or from a Task component is not supported.
Initialize the SDK with your api key. You typically do this inside the init()
method of your main scene.
sub init()
Purchases().configure({
"apiKey": "roku_XXXXX"
})
end sub
In methods of the SDK which perform async operations, you can get the result by passing a sub routine or a callback name.
- The first parameter will contain the result.
- The second parameter will contain an error if there was one, or
invalid
if there wasnt.
Example:
sub init()
Purchases().logIn(my_user_id, sub(subscriber, error)
if error <> invalid
print "there was en error"
else
print subscriber
end if
end sub)
' To use a function as callback, pass its name as second parameter
Purchases().logIn(my_user_id, "onSubscriberReceived")
end sub
sub onSubscriberReceived(e as object)
data = e.GetData()
if data.error <> invalid
print "there was en error"
else
print data.result
end if
end sub
The subscriber object is returned from different APIs. Here's an example of what it looks like:
{
activeSubscriptions: ["my_product_id"]
allExpirationDatesByProduct: {
"my_product_id": <Component: roDateTime>
}
allPurchaseDatesByProduct: {
"my_product_id": <Component: roDateTime>
}
allPurchasedProductIds: ["my_product_id"]
entitlements: {
all: {
billingIssueDetectedAt: invalid
expirationDate: <Component: roDateTime>
identifier: "premium"
isActive: false
isSandbox: true
latestPurchaseDate: <Component: roDateTime>
originalPurchaseDate: <Component: roDateTime>
ownershipType: "PURCHASED"
periodType: "normal"
productIdentifier: "my_product_identifier"
productPlanIdentifier: invalid
store: "app_store"
unsubscribeDetectedAt: invalid
willRenew: false
}
active: {}
}
firstSeen: <Component: roDateTime>
lastSeen: <Component: roDateTime>
latestExpirationDate: <Component: roDateTime>
managementUrl: invalid
nonSubscriptionTransactions: [
{
isSandbox: false
originalPurchaseDate: <Component: roDateTime>
purchaseDate: <Component: roDateTime>
store: "roku"
storeTransactionIdentifier: "XXXXXXX"
transactionIdentifier: "XXXXXXX"
productIdentifier: "my_product_id"
}
]
originalAppUserId: "$RCAnonymousID:XXXXXXXXXXXXXXXX"
originalApplicationVersion: "1.0"
originalPurchaseDate: <Component: roDateTime>
requestDate: <Component: roDateTime>
}
The error model constains two fields: code
and message
{
code: 1234,
message: "There as an error",
}
As a parameter to the purchase()
method, you can pass an associative array containing one of the following values:
code
: A string containing the product id.product
: From thegetOfferings
result: e.g.offerings.current().annual.storeProduct
package
: From thegetOfferings
result: e.g.offerings.current().annual
Additionally,you can pass the following optional parameters:
action
: To perform a product change. Valid values:Upgrade
orDowngrade
Purchases().purchase({ code: "product_id" }, sub(result, error)
' error will be present if the transaction could not be finished
if error <> invalid
if result <> invalid and result.userCancelled = true
print "The user cancelled the purchase"
end if
print "The purchase could not be completed"
else
' The raw transaction generated by the purchase
result.transaction
' {
' amount: "$0.00"
' code: "yearly_subscription_product"
' description: "Yearly Subscription"
' externalCode: ""
' freeTrialQuantity: 0
' freeTrialType: "None"
' name: "Yearly Subscription"
' originalAmount: "0"
' productType: "YearlySub"
' promotionApplied: false
' purchaseId: "00000000-0000-0000-0000-000000000000"
' qty: 1
' replacedOffers: []
' replacedSubscriptionId: ""
' rokuCustomerId: "00000000-0000-0000-0000-000000000000"
' total: "$0.00"
' trialCost: "$0.99"
' trialQuantity: 1
' trialType: "Years"
' }
' The subscriber object
result.subscriber
end
end sub)
Purchases().getOfferings(sub(offerings, error)
if error <> invalid
print "There was an error fetching offerings
else
' The offerings object
' {
' current(): {
' identifier: "my_id",
' metadata: { }, ' Metadata set in the Offering configuration
' description: "Offering description",
' annual: {}, ' The configured Annual package, if available
' monthly: {}, ' The configured Monthly package, if available
' ' A list of all available packages
' availablePackages: [
' {
' identifier: "package_identifier",
' packageType: "custom",
' ' The raw Roku store product
' storeProduct: {
' code: "yearly_subscription_product"
' cost: "$1.99"
' description: "Yearly Subscription"
' freeTrialQuantity: 0
' freeTrialType: "None"
' HDPosterUrl: ""
' id: "00000000-0000-0000-0000-000000000000"
' inStock: "true"
' name: "Yearly Subscription"
' offerEndDate: ""
' offerStartDate: ""
' productImagePortrait: ""
' productImageUrl: ""
' productType: "YearlySub"
' qty: 0
' SDPosterUrl: ""
' trialCost: "$0.99"
' trialQuantity: 12
' trialType: "Months"
' }
' }
' ],
' },
' all: {
' ' An associative array of all the offerings, keyed by their identifier
' }
' }
end if
end sub)
You can also retrieve the current offering for a placement identifier. Use this to access offerings defined by targeting placements configured in the RevenueCat dashboard:
Purchases().getOfferings(sub(offerings, error)
if error <> invalid
print "There was an error fetching offerings
else
my_offering = offerings.currentOfferingForPlacement("my_placement")
end if
end sub)
' subscriber: The new user subscriber info
' error: Will be present if there was an error during the process
Purchases().logIn("my_user_id", sub(subscriber, error)
end sub)
' Calling logOut generated a new anonymous user
' subscriber: The new anonymous user subscriber info
' error: Will be present if there was an error during the process
Purchases().logOut(sub(subscriber, error)
end sub)
' subscriber: The current subscriber info
' error: Will be present if there was an error during the process
Purchases().getSubscriberInfo(sub(subscriber, error)
end sub)
' success: Will be true if the attributes were successfully synchronized
' error: Will be present if there was an error during the process
Purchases().setAttributes({ "my attribute": "my value" }, sub(success, error)
end sub)
This method will post all purchases associated with the current Roku account to RevenueCat and become associated with the current User ID. It should only be used if you're migrating from using your own Roku Pay implementation and want to track previous purchases in RevenueCat
' subscriber: The current subscriber info
' error: Will be present if there was an error during the process
Purchases().syncPurchases(sub(subscriber, error)
end sub)
For most apps, the usage of the SDK would look like this:
- Initialise the SDK
- Log in the user
- Check if the entitlement is active
- Fetch offerings and show your paywall UI
- Make a purchase
sub init()
' Initialize the SDK
Purchases().configure({
"apiKey": "roku_XXXXX",
})
' Login the user
Purchases().logIn(m.my_user_id, sub(subscriber, error)
if error = invalid
' If my entitlement is not active, fetch offerings to show the paywall
if subscriber.entitlements.my_entitlement.isActive = false
fetchOfferings()
end if
end if
end sub)
end sub
sub fetchOfferings()
Purchases().getOfferings(sub(offerings, error)
if error = invalid
' Use offerings to build your paywall UI.
' Then call purchaseProduct with the one selected by the user
purchaseProduct(offerings.current().annual)
end if
end sub)
end sub
' Call purchaseProduct when the user decides to initiate a purchase
sub purchaseProduct(product)
Purchases().purchase(product, sub(result, error)
if error = invalid
print "Purchase successful"
print result.transaction
print result.subscriber
end if
end sub)
end sub