├── README.md
├── components
├── ChannelStoreProduct.xml
├── ChannelStoreProductData.xml
├── MainScene.brs
├── MainScene.xml
├── RegTask.brs
├── RegTask.xml
├── UriFetcher.brs
├── UriFetcher.xml
├── content
│ ├── ContentScreen.brs
│ ├── ContentScreen.xml
│ ├── FeedParser.brs
│ ├── FeedParser.xml
│ └── PosterItem.xml
└── screens
│ ├── SignInScreen.brs
│ ├── SignInScreen.xml
│ ├── SignUpScreen.brs
│ └── SignUpScreen.xml
├── images
├── background.jpg
├── icon_focus_hd.jpg
├── purchased_poster.png
├── roku-developers-top.jpg
└── splash_hd.jpg
├── manifest
├── rasp
├── OnDeviceAuthenticationSignIn.rasp
└── OnDeviceAuthenticationSignOut.rasp
└── source
├── main.brs
└── utils.brs
/README.md:
--------------------------------------------------------------------------------
1 | # On-device-authentication
2 |
3 | ## Overview
4 |
5 | This channel demonstrates how to implement on-device signups and sign-ins and grant customers access to content. It shows how to use the [**ChannelStore node**](https://developer.roku.com/docs/references/scenegraph/control-nodes/channelstore.md) and [**Roku Web Service API**](https://developer.roku.com/docs/developer-program/roku-pay/roku-web-service.md) to check for an active Roku subscription, and how to use the [**roRegistrySection()**](https://developer.roku.com/docs/references/brightscript/components/roregistrysection) object and [**ChannelStore node**](https://developer.roku.com/docs/references/scenegraph/control-nodes/channelstore.md) to check for access tokens in the device registry and Roku Cloud, respectively. If the customer does not have an active subscription or their subscription cannot be validated (because it was purchased on a different platform), the sample shows how to use the Roku Pay [Request for Information (RFI) screen](https://developer.roku.com/docs/references/scenegraph/control-nodes/channelstore.md#getuserdata) to sign customers up for a new Roku subscription and sign them in to their existing subscription.
6 |
7 | > You must incorporate this sample with the [products](https://developer.roku.com/docs/developer-program/roku-pay/quickstart/in-channel-products.md) and [test users](https://developer.roku.com/docs/developer-program/roku-pay/quickstart/test-users.md) linked to your channel to observe its entire functionality. Your channel must also be enabled for [billing testing](https://developer.roku.com/docs/developer-program/roku-pay/testing/billing-testing.md).
8 |
9 | ## Components
10 |
11 | ### MainScene
12 |
13 | The **MainScene** component validates whether the Roku account linked to the device has a purchased a subscription for the sample channel. If a customer does not have a subscription or their subscription cannot be validated, it displays a landing screen page where customers can sign up for a subscription or sign in to their existing one. It monitors the **Sign Up** and **Sign In** buttons on the landing page to determine which RFI screen context ("signup" or "signin") to display when a button is pressed. For sign-ups, it creates a new subscriptions through Roku Pay; for sign-ins, it verifies the credentials and then grants access to content. The logic used in this component follows the [On-Device Authentication](https://developer.roku.com/docs/developer-program/authentication/on-device-authentication.md#overview) document to verify access to content and create new Roku subscriptions.
14 |
15 | > This sample uses hardcoded values for the access token and email validation flag instead of calls to authentication and entitlement server endpoints. You can replace these hardcoded values with calls to your endpoints to test your services with this sample channel.
In this case, you must change the **bs_const=sampleHardCodedValues** flag in the channel manifest to false, replace the sample API key ("devAPIKey") in the **MainScene.brs** file with your [developer API key](https://developer.roku.com/api/settings), and provide the URLs of your endpoints in the commented out **makeRequest()** methods (these include a field with a placeholder value such as "PUBLISHER...LINK GOES HERE").
16 |
17 | ### SignUpScreen and SignInScreen
18 |
19 | The **SignUpScreen** component is used to handle customers that click **Cancel** on the RFI screen when signing up for a subscription because they do not want to share their Roku customer account information with the channel. This component enables customers to manually enter their email address in a [StandardKeyboardDialog](https://developer.roku.com/docs/references/scenegraph/standard-dialog-framework-nodes/standard-keyboard-dialog.md); the entered email address is then displayed in a [TextEditBox](https://developer.roku.com/docs/references/scenegraph/widget-nodes/texteditbox.md). When the customer clicks **Sign Up**, the component returns the customer to the **MainScene** component to purchase a subscription.
20 |
21 | The **SignInScreen** component is used to collect the customer's email address and password after they click **Sign In** on the landing page. If the customer elects to share the email address associated with their Roku customer account information in the RFI screen, it pre-populates a [TextEditBox](https://developer.roku.com/docs/references/scenegraph/widget-nodes/texteditbox.md) with the customer's email address, and then displays a StandardKeyboardDialog for them to enter their password. The obfuscated password is displayed in a TextEditBox. If the customer clicks **Use different email** in the RFI screen, it displays a [StandardKeyboardDialog](https://developer.roku.com/docs/references/scenegraph/standard-dialog-framework-nodes/standard-keyboard-dialog.md) for them to enter their email address. When the customer clicks **Sign In**, the channel validates the credentials, and then displays the **ContentScreen** component, which displays video content that can be played.
22 |
23 | ### ChannelStoreProduct
24 |
25 | The **ChannelStoreProduct** component provides the user interface for this sample channel. It includes **LayoutGroup** nodes for displaying the names, codes, and prices for the mock products, and it includes a **Poster** node for marking products as "purchased" when they are bought.
26 |
27 | ### RegTask
28 |
29 | The **RegTask** component provides methods for reading, writing, and deleting this sample channel's registry section on a Roku device.
30 |
31 | ### UriFetcher
32 |
33 | The **UriFetcher** component provides a simple, non-blocking implementation of a basic download manager which is capable of multiple asynchronous URL requests implemented through a long-lived data task. This can be used in on-device authentication to make asynchronous calls to authentication and entitlement services, for example.
34 |
35 | ### Feed Parser
36 |
37 | The **FeedParser** component retrieves the video content items used in this sample from an XML feed and then indexes and transforms them into ContentNodes so they can be displayed in BrightScript components. This process is useful if you do not have a web service for pulling content IDs from your feed (for example, your feed is maintained in an Amazon S3 bucket).
38 |
39 | Specifically, the **FeedParser** component stores the stream URLs and other meta data for each content item in the feed in an array of associative arrays. The captured meta data includes a thumbnail image (used for the channel's poster and background images), description, and title. Importantly, it stores the items' GUIDs as content IDs and links them to the metadata. This enables you to pass the GUIDs in ECP cURL commands and deep link into the associated content. To display the content items on the screen, it formats the items into ContentNodes and then populates them in a RowItem.
40 |
41 | ### ContentScreen
42 |
43 | The **ContentScreen** component displays and plays video content once a customer's subscription purchase is validated.
44 |
45 | ## Installation
46 |
47 | To run this sample, follow these steps:
48 |
49 | 1. Download and then extract the sample.
50 |
51 | 2. Expand the extracted **On-Device-Authentication-Sample** folder, and then compress the contents in to a ZIP file.
52 |
53 | 3. Follow the steps in [Loading and Running Your Application](https://developer.roku.com/docs/developer-program/getting-started/developer-setup.md#step-1-set-up-your-roku-device-to-enable-developer-settings) to enable developer mode on your device and sideload the ZIP file containing the sample onto it. Optionally, you can launch the sample channel via the device UI. The sample channel is named **On Device Authentication Sample (dev)**.
54 |
55 | 4. Upon launching the channel, click **Sign Up**. In the RFI screen, click **Continue** to grant the sample channel access to your Roku customer account information (or click **Cancel** to manually enter your information).
56 |
57 | 5. Click a product in the sample channel UI. Complete the transaction for the product. The product will be marked as "Purchased" when you open the sample channel again.
58 |
59 | > This sample requires that you to use the [in-channel products](https://developer.roku.com/products) linked to your development channel.
60 |
61 | > Your device must be running Roku OS 9.2 or greater to ensure that the sample channel runs properly after cancelling mock purchases in the Roku Pay screen.
62 |
63 | 6. Access is granted to the channel's video catalog.
64 |
65 | 7. To re-purchase products, replace the Api Key (devAPIKey) in the **MainScene.brs** file with your developer API key. Press the Option (*) button on the Roku remote control to delete the sample channel's registry, and then go to the [Test Users](https://developer.roku.com/users) page in the Developer Dashboard to void the product transactions associated with a test user linked to your channel.
66 |
67 | 8. To test the sign-in flow upon launching the channel, click **Sign In**. In the RFI screen, click **Continue** to grant the sample channel access to the email address associated with your Roku customer account (or click **Use different email** to manually enter it). Enter a password, and then click **Sign In**.
68 |
69 |
--------------------------------------------------------------------------------
/components/ChannelStoreProduct.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
32 |
33 |
34 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/components/ChannelStoreProductData.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/components/MainScene.brs:
--------------------------------------------------------------------------------
1 | sub init()
2 | m.store = m.top.findNode("store")
3 | m.store.ObserveField("catalog", "onGetCatalog")
4 | m.store.observeField("orderStatus", "onOrderStatus")
5 | m.store.ObserveField("purchases", "onGetPurchases")
6 | m.store.ObserveField("userData", "onGetUserData")
7 |
8 | m.store.ObserveField("storeChannelCredDataStatus", "onStoreChannelCredData")
9 | m.store.ObserveField("channelCred", "onGetChannelCred")
10 |
11 | m.productGrid = m.top.FindNode("productGrid")
12 | m.productGrid.ObserveField("itemSelected", "onProductSelected")
13 |
14 | m.productSelectScreen = m.top.FindNode("productSelectScreen")
15 |
16 | ' Added account creation initial screens'
17 | m.landingScreen = m.top.FindNode("landingScreen")
18 | m.landingButtonGroup = m.top.FindNode("landingButtonGroup")
19 |
20 | m.signInScreen = m.top.FindNode("signInScreen")
21 | m.signInButtonGroup = m.top.FindNode("signInButtonGroup")
22 |
23 | m.contentScreen = m.top.FindNode("contentScreen")
24 | m.contentScreenRowList = m.top.FindNode("RowList")
25 |
26 | ' TODO: this shouldn't need to be set visible/invisible
27 | m.selectHint = m.top.FindNode("selectHint")
28 | m.hint = m.top.FindNode("hint")
29 |
30 | m.top.observeField("response", "onDataRequestResponse")
31 |
32 | ' Specify RFI type when handling userData'
33 | m.rfiType = "None"
34 |
35 | m.uriFetcher = createObject("roSGNode", "UriFetcher")
36 |
37 | m.registryTask = CreateObject("roSGNode", "regTask")
38 | m.registryTask.control = "run"
39 |
40 | 'm.keyboard = CreateObject("roSGNode", "KeyboardDialog")
41 | 'm.keyboard.id = "KeyboardDialog"
42 | 'm.keyboard.observeField("buttonSelected","onConfirmEmail")
43 |
44 | m.dialogBox = CreateObject("roSGNode", "Dialog")
45 | m.dialogBox.id = "dialogBox"
46 | m.dialogBox.observeField("buttonSelected","dismissdialog")
47 |
48 | m.init = true
49 | m.publisherEntitlement = invalid
50 | m.devEntitlement = invalid
51 | m.publisherAccessToken = invalid
52 | m.itemSelected = invalid
53 | m.clearTokenReq = false
54 | m.devAPIKey = "DEV API KEY GOES HERE"
55 |
56 | ' check and see if any previous purchases have been made
57 | m.store.command = "getPurchases"
58 | end sub
59 |
60 | function onGetPurchases() as Void
61 | ?"> onGetPurchases"
62 | ' check with the current catalog to see if previous purchases exist
63 | if m.init
64 | m.store.command = "getCatalog"
65 | return
66 | end if
67 |
68 | if (m.store.purchases.GetChildCount() > 0)
69 | ' there exist purchases, do a quick check
70 | ' already updated pre existing purchased products
71 | ? "verifying access to content (during purchase)"
72 | ' validate the publisher information and the developer information
73 | verifyAccessToContent()
74 |
75 | return
76 | end if
77 | ' no active subscriptions b/c no purchases made
78 | ? "customer does NOT have active subscription through roku pay"
79 | ' grab the access token (if any) from registry and verify with the publisher info
80 | makeRequest("registry", {section: "sample", command: "read", key: "sample_access_token", value: ""}, "validateInactiveRokuSub")
81 | end function
82 |
83 | function onGetCatalog() as void
84 | ? "> onGetCatalog"
85 | data = CreateObject("roSGNode", "ContentNode")
86 | if (m.store.catalog <> invalid)
87 | count = m.store.catalog.GetChildCount()
88 | for i = 0 to count - 1
89 | productData = data.CreateChild("ChannelStoreProductData")
90 | item = m.store.catalog.getChild(i)
91 | productData.productCode = item.code
92 | productData.productName = item.name
93 | productData.productPrice = item.cost
94 | productData.productBought = false
95 |
96 | if m.init 'check catalog and purchases and see if any are already purchased
97 | for x = 0 to (m.store.purchases.GetChildCount() - 1)
98 | p_code = m.store.purchases.getChild(x).getFields().code
99 | if productData.productCode = p_code
100 | 'already purchased, indicate it visually
101 | productData.productBought = true
102 | 'break out of for loop
103 | x = m.store.purchases.GetChildCount() - 1
104 | end if
105 | end for
106 | end if
107 |
108 | end for
109 | m.productGrid.content = data
110 | end if
111 | if m.init
112 | ' validate the publisher information and the developer information
113 | verifyAccessToContent()
114 | end if
115 | m.init = false
116 | ' Check content access if didn't purchase on Roku
117 | end function
118 |
119 | function onProductSelected() as void
120 | ? "!----------------------new order---------------------!"
121 | ? "> onProductSelected"
122 | index = m.productGrid.itemSelected
123 | m.itemSelected = m.productGrid.content.GetChild(index)
124 | ? "> selected product code: " m.itemSelected.productCode
125 |
126 | ' query the publisher server on information on the selected product
127 | #if sampleHardCodedValues
128 | ? "< getting publisher information"
129 | m.publisherEntitlement = "true"
130 | m.publisherAccessToken = "TOK8ZQEDDR8AWVJF8AH"
131 | ?"< publisher is entitled " m.publisherEntitlement
132 | ?"< publisher token: " m.publisherAccessToken
133 | ' check roku side if this item has already been purchased
134 | m.store.command = "getPurchases"
135 | #else
136 | makeRequest("url", {uri: "PUBLISHER ENTITLEMENT LINK GOES HERE"}, "getPublisherInfo")
137 | #end if
138 | end function
139 |
140 | function getPublisherInfo(msg as Object)
141 | ? "< getting publisher information"
142 | response = msg.getData()
143 | m.publisherEntitlement = ParseJson(response.content).entitlement
144 | ?"< publisher is entitled " m.publisherEntitlement
145 | makeRequest("url", {uri: "PUBLISHER TOKEN KEY LINK GOES HERE"}, "getPublisherInfoComplete")
146 | end function
147 |
148 | function getPublisherInfoComplete(msg as Object)
149 | response = msg.getData()
150 | m.publisherAccessToken = ParseJson(response.content).tokenKey
151 | ?"< publisher token: " m.publisherAccessToken
152 |
153 | ' check roku side if this item has already been purchased
154 | m.store.command = "getPurchases"
155 | end function
156 |
157 | function verifyAccessToContent() as Void
158 | count = m.store.purchases.GetChildCount()
159 | ? "[user already purchased " count " item(s)]"
160 | ' find a match in the list of purchases made
161 | for i = 0 to count - 1
162 | #if sampleHardCodedValues
163 | 'xml = createObject("roXMLElement")
164 | 'xml.parse(response.content)
165 | m.devEntitlement = true
166 | ? "< dev is entitled: " m.devEntitlement
167 | makeRequest("registry", {section: "sample", command: "read", key: "sample_access_token", value: ""}, "getDevInfoComplete")
168 | #else
169 | ' Is this check for purchased product code really needed?'
170 | 'if (m.store.purchases.getChild(i).getFields().code = m.itemSelected.productCode)
171 | ? "customer has active subscription through roku pay"
172 | 'is an active subscription through Roku Pay
173 | tid = m.store.purchases.getChild(i).getFields().purchaseId
174 |
175 | 'check device has valid access token and entitlment in publisher system, query apipublroku.com for entitlement info and the registry for access token
176 | makeRequest("url", {uri: Substitute("https://apipub.roku.com/listen/transaction-service.svc/validate-transaction/{0}/{1}", m.devAPIKey, tid)}, "getDevInfo")
177 | return
178 | 'end if
179 | #end if
180 | end for
181 |
182 | ' not an active subscription b/c no matching products
183 | ? "customer does NOT have active subscription through roku pay"
184 | ' grab the access token (if any) from registry and verify with the publisher info
185 | makeRequest("registry", {section: "sample", command: "read", key: "sample_access_token", value: ""}, "validateInactiveRokuSub")
186 | end function
187 |
188 | ' handle response from validate-transaction'
189 | function getDevInfo(msg as Object) as void
190 | ? "< getting device info: getDevInfo"
191 | response = msg.getData()
192 | if response.code <> 200
193 | ? "Validate transaction failed, check if API key and transaction ID are valid"
194 | ' dialogBox = CreateObject("roSGNode", "Dialog")
195 | m.dialogBox.message = "Error: " + chr(10) + "Validate transaction failed: check if API key and transaction ID are valid"
196 | m.dialogBox.buttons = ["Go back to Channel Add-ons UI"]
197 | m.top.dialog = m.dialogBox
198 | return
199 | end if
200 |
201 | xml = createObject("roXMLElement")
202 | xml.parse(response.content)
203 | m.devEntitlement = (xml.getNamedElements("isEntitled")[0].getText() = "true")
204 | ? "< dev is entitled: " m.devEntitlement
205 | makeRequest("registry", {section: "sample", command: "read", key: "sample_access_token", value: ""}, "getDevInfoComplete")
206 | end function
207 |
208 | function getDevInfoComplete(msg as Object)
209 | tok = msg.getData().regVal
210 | ? "> dev access tok: " tok
211 | ' obtained the dev access token and entitlement, now validate the info
212 | validateAccessToken(tok, m.devEntitlement)
213 | end function
214 |
215 | function validateAccessToken(tok as Object, entitled as Boolean)
216 | ' check for matching in the developer and publisher
217 | if (tok <> "invalid")
218 | if ((tok = m.publisherAccessToken) and (m.publisherEntitlement = "true"))
219 | ? "device has valid access token and entitlement in publisher system"
220 | ' get access token from publisher server and store on device
221 | #if sampleHardCodedValues
222 | m.publisherAccessToken = "TOK8ZQEDDR8AWVJF8AH"
223 | writeAccessToken()
224 | #else
225 | makeRequest("url", {uri: "PUBLISHER TOKEN KEY LINK GOES HERE"}, "getWriteAccessToken")
226 | #end if
227 |
228 | grantAccess()
229 | end if
230 | else 'either not valid access token or no entitlement in publisher
231 | ? "device either doesn't have valid publisher access token and/or no entitlement in publisher system"
232 | ' get publisher access token from publisher server and store on device
233 | writeAccessToken()
234 | grantAccess()
235 |
236 | end if
237 | end function
238 |
239 | function createOrder() as void
240 | ' create, process, and validate order
241 | myOrder = CreateObject("roSGNode", "ContentNode")
242 | itemPurchased = myOrder.createChild("ContentNode")
243 | ? "creating order ..."
244 | ? "> product code: " m.itemSelected.productCode
245 | ? "> product name: " m.itemSelected.productName
246 | itemPurchased.addFields({ "code": m.itemSelected.productCode, "name": m.itemSelected.productName, "qty": 1})
247 | m.store.order = myOrder
248 | ? "processing order ..."
249 | m.store.command = "doOrder"
250 | end function
251 |
252 | function validateInactiveRokuSub(msg as Object)
253 | ? "> validateInactiveRokuSub"
254 | tok = msg.getData().regVal
255 | ? "> dev access tok: " tok
256 |
257 | ' validate purchaser access token and publisher system entitlement
258 | if (tok <> "invalid")
259 | 'if ((tok = m.publisherAccessToken) and (m.publisherEntitlement = "true"))
260 | ? "device has valid access token and entitlement in publisher system"
261 | grantAccess()
262 | 'end if
263 | else ' device registry does not have valid purchaser access token and publisher system has entitlement
264 | ? "device either doesn't have valid access token and/or no entitlement in publisher system"
265 | if m.itemSelected = invalid
266 | m.store.command = "getChannelCred"
267 | else
268 | createOrder()
269 | end if
270 | end if
271 | end function
272 |
273 | function onGetChannelCred()
274 | print "> is access token stored in Roku Cloud?"
275 | ' if token matches - '
276 | if (m.store.channelCred <> invalid)
277 | if m.store.channelCred.status = 0
278 | if m.store.channelCred.json <> invalid and m.store.channelCred.json <> "{}"
279 | json = parsejson(m.store.channelCred.json)
280 | if (json <> invalid) and (json.roku_pucid <> invalid and json.roku_pucid <> "{}")
281 | ' check that json.token_type is urn:roku:pucid:token_type:pucid_token'
282 | tok = json.channel_data
283 | print "channel cred= "; json
284 | if ((tok = m.publisherAccessToken) and (m.publisherEntitlement = "true"))
285 | 'write publisher token to registry'
286 | print "Yes - token store in cloud"
287 | writeAccessToken()
288 | else
289 | 'create new subscription through rokupay
290 | ' get customer's email address
291 | 'print "No - go to create new subscription"
292 | 'm.store.command = "getUserData"
293 | print "No - go to select sign up/in screen"
294 | displayLandingScreen()
295 | end if
296 | end if
297 | end if
298 | else
299 | print "non-zero status = "; m.store.channelCred.status
300 | end if
301 | end if
302 | end function
303 |
304 | function displayLandingScreen()
305 | print "Display Sign up and Sign in buttons"
306 | m.landingButtonGroup.setFocus(true)
307 |
308 | Buttons = ["Sign Up","Sign In"]
309 | m.landingButtonGroup.buttons = Buttons
310 | m.landingButtonGroup.observeField("buttonSelected","onLandingButtonSelected")
311 | m.landingScreen.visible = true
312 | end function
313 |
314 | function onLandingButtonSelected()
315 | m.landingScreen.visible = false
316 | if m.landingButtonGroup.buttonSelected = 0
317 | ' sign up button pressed'
318 | print "Request sign up RFI"
319 | m.rfiType = "signup"
320 | m.store.requestedUserData = "email"
321 | m.store.command = "getUserData"
322 | else if m.landingButtonGroup.buttonSelected = 1
323 | ' sign in button pressed'
324 | print "Request sign in RFI"
325 | m.rfiType = "signin"
326 | ' Set sign-in context for RFI screen
327 | info = CreateObject("roSGNode", "ContentNode")
328 | info.addFields({context: "signin"})
329 | m.store.requestedUserDataInfo = info
330 |
331 | m.store.requestedUserData = "email"
332 | m.store.command = "getUserData"
333 | 'else
334 | ' return to main screen'
335 | 'm.productSelectScreen.visible = "true"
336 | 'm.productGrid.SetFocus(true)
337 | end if
338 | end function
339 |
340 | function displaySignInScreen(email as String)
341 | print "Sign in screen - email= "; email
342 | signinScreen = m.top.findNode("SignInScreen")
343 | if signinScreen <> invalid
344 | signinScreen.email = email
345 | signinScreen.setup = true
346 | signinScreen.visible = true
347 | end if
348 | 'signinScreen.findNode("signinKeyboard").setFocus(true)
349 | end function
350 |
351 | function displaySignUpScreen(email as String)
352 | print "Sign up screen - email= "; email
353 | signupScreen = m.top.findNode("SignUpScreen")
354 | if signupScreen <> invalid
355 | signupScreen.email = email
356 | signupScreen.setup = true
357 | signupScreen.visible = true
358 | end if
359 | end function
360 |
361 | sub onDataRequestResponse(msg)
362 | print "entered onDataRequestResponse"
363 | print "data= "; msg.getData()
364 | ' handle the data and set focus on product markup grid'
365 | 'm.productGrid.SetFocus(true)
366 | onConfirmEmail(msg)
367 | end sub
368 |
369 | ' This is the function that handles the RFI result'
370 | function onGetUserData()
371 | if m.rfiType = "signup"
372 | if (m.store.userData <> invalid)
373 | email = m.store.userData.email
374 | ? "email of user is: " email
375 | hashedPass = hashThePassword("")
376 | responseAA = {type:"signup", email:email, password:hashedPass}
377 | 'trigger confirm email path'
378 | m.top.response = responseAA
379 |
380 | ''? "> create new subscription through roku pay"
381 |
382 | 'm.keyboard.title = "Sign In"
383 | ' m.keyboard.text = email
384 | ' m.keyboard.buttons = ["Confirm"]
385 | ' m.keyboard.opacity = 0.95
386 | ' m.top.dialog = m.keyboard
387 | else
388 | ? "[user cancelled obtaining email, enter email manually]"
389 | email = ""
390 | displaySignUpScreen(email)
391 | end if
392 | ' Let user retry sign up or sign in'
393 | else if m.rfiType = "signin"
394 | if (m.store.userData <> invalid)
395 | email = m.store.userData.email
396 | ? "email of user is: " email
397 | else
398 | ? "[user cancelled obtaining email, returning to displaying channel UI]"
399 | email = ""
400 | end if
401 | displaySignInScreen(email)
402 | end if
403 | m.rfiType = "None"
404 | end function
405 |
406 | function onConfirmEmail(msg as Object)
407 | ? "> confirming email ..."
408 | 'dismissdialog()
409 | ' check if email address linked to active subscription in publisher's system, logic should be on the publisher side
410 | #if sampleHardCodedValues
411 | isLinkedEmail(msg)
412 | #else
413 | m.progressdialog = createObject("roSGNode", "ProgressDialog")
414 | m.progressdialog.title = "Linking Email ..."
415 | m.top.dialog = m.progressdialog
416 |
417 | makeRequest("url", {uri: "PUBLISHER EMAIL VERIFICATION LINK GOES HERE"}, "isLinkedEmail")
418 | 'adjust isLinkedEmail to handle the response from publisher system'
419 | #end if
420 | end function
421 |
422 | function isLinkedEmail(msg as Object)
423 | rspData = msg.getData()
424 | if rspData.type = "signup" then
425 | ' need to send to publisher system to create account
426 | isLinked = "false"
427 | else
428 | ' type = "signin" - check with publisher system
429 | 'isLinked = msg.getData().content
430 | isLinked = "true"
431 | end if
432 |
433 | if isLinked = "true" 'email is linked to active subscription
434 | ? "email is linked to an active subscription in publisher system"
435 | #if sampleHardCodedValues
436 | m.publisherAccessToken = "TOK8ZQEDDR8AWVJF8AH"
437 | writeAccessToken()
438 | #else
439 | ' get access token from publisher server and store on device
440 | dismissdialog()
441 | makeRequest("url", {uri: "PUBLISHER TOKEN KEY LINK GOES HERE"}, "getWriteAccessToken")
442 | #end if
443 | grantAccess()
444 | else
445 | ? "email not linked to active subscription in publisher system, create and process an order ..."
446 | m.productSelectScreen.visible = "true"
447 | m.hint.visible = "true"
448 | m.selectHint.visible = "true"
449 | m.productGrid.setFocus(true)
450 | end if
451 | end function
452 |
453 | function onOrderStatus(msg as Object)
454 | status = msg.getData().status
455 | if status = 1 ' order success
456 | ? "> order success"
457 | #if sampleHardCodedValues
458 | m.publisherAccessToken = "TOK8ZQEDDR8AWVJF8AH"
459 | writeAccessToken()
460 | #else
461 | tid = m.store.orderStatus.getChild(0).purchaseId
462 | ' validate the order by checking if it is now entitled on the roku side
463 | makeRequest("url", {uri: Substitute("https://apipub.roku.com/listen/transaction-service.svc/validate-transaction/{0}/{1}", m.devAPIKey, tid)}, "validateOrder")
464 | #end if
465 | else 'error in doing order
466 | ? "> order error ..."
467 | ? "> error status " status ": " msg.getData().statusMessage
468 | m.dialogBox.message = "Order Error: " + chr(10) + msg.getData().statusMessage
469 | m.dialogBox.buttons = ["Go back to Channel Add-ons UI"]
470 | m.top.dialog = m.dialogBox
471 | m.store.order = invalid 'clear order
472 | end if
473 | end function
474 |
475 | function validateOrder(msg as Object) as void
476 | ? "validating order ..."
477 | response = msg.getData()
478 | if response.code <> 200
479 | ? "Validate transaction failed, check if API key and transaction ID are valid"
480 | ' dialogBox = CreateObject("roSGNode", "Dialog")
481 | m.dialogBox.message = "Error: " + chr(10) + "Validate transaction failed: check if API key and transaction ID are valid"
482 | m.dialogBox.buttons = ["Go back to Channel Add-ons UI"]
483 | m.top.dialog = m.dialogBox
484 | return
485 | end if
486 |
487 | xml = createObject("roXMLElement")
488 | xml.parse(response.content)
489 | isEntitled = (xml.getNamedElements("isEntitled")[0].getText() = "true")
490 | ? "< new purchase is entitled: " isEntitled
491 | if isEntitled = true
492 | print "order is entitled, store access token on device and grant access to user"
493 | m.itemSelected.productBought = true
494 | #if sampleHardCodedValues
495 | m.publisherAccessToken = "TOK8ZQEDDR8AWVJF8AH"
496 | writeAccessToken()
497 | #else
498 | makeRequest("url", {uri: "PUBLISHER TOKEN KEY LINK GOES HERE"}, "getWriteAccessToken")
499 | #end if
500 |
501 | 'grantAccess()
502 | else
503 | ? "order is not entitled, create new subscription again"
504 | ' dialogBox = CreateObject("roSGNode", "Dialog")
505 | m.dialogBox.message = "Error: " + chr(10) + "device is not entitled"
506 | m.dialogBox.buttons = ["Go back to Channel Add-ons UI"]
507 | m.top.dialog = m.dialogBox
508 | end if
509 | end function
510 |
511 | function getWriteAccessToken(msg as Object)
512 | ' get and write access token from publisher server and store on device
513 | m.publisherAccessToken = ParseJson(msg.getData().content).tokenKey
514 | ? "< writing access token from publisher server " m.publisherAccessToken
515 | makeRequest("registry" , {section: "sample", command: "write", key: "sample_access_token", value: m.publisherAccessToken }, "onAccessTokenWrite")
516 | end function
517 |
518 | function writeAccessToken()
519 | ' write access token from publisher server and store on device
520 | ? "< writing access token from publisher server " m.publisherAccessToken
521 | makeRequest("registry" , {section: "sample", command: "write", key: "sample_access_token", value: m.publisherAccessToken }, "onAccessTokenWrite")
522 | print " also write the access token in Roku Cloud"
523 | m.store.channelCredData = m.publisherAccessToken
524 | m.store.command = "storeChannelCredData"
525 | end function
526 |
527 | function onAccessTokenWrite(msg)
528 | print "> finished writing access token" msg
529 | end function
530 |
531 | function onStoreChannelCredData() as void
532 | print "> finished storing access token in Roku Cloud"
533 | if (m.store.storeChannelCredDataStatus <> invalid)
534 | print "- response: " m.store.storeChannelCredDataStatus.response
535 | print "- status: " m.store.storeChannelCredDataStatus.status
536 | end if
537 | ' Grant access to the user'
538 | if m.clearTokenReq = true
539 | m.clearTokenReq = false
540 | else
541 | grantAccess()
542 | end if
543 | end function
544 |
545 | sub dismissdialog()
546 | m.top.dialog.close = true
547 | end sub
548 |
549 | function grantAccess()
550 | m.dialogBox.message = "Success: " + chr(10) + "Access Granted!"
551 | m.dialogBox.buttons = ["Continue"]
552 | m.top.dialog = m.dialogBox
553 | print "!-------------------access granted-------------------!"
554 | ' hide product screen and display the content screen'
555 | m.productSelectScreen.visible = false
556 | m.selectHint.text = "Navigate content grid"
557 | m.selectHint.visible = true
558 | m.hint.visible = true
559 | m.contentScreen.visible = true
560 | m.contentScreenRowList.SetFocus(true)
561 | end function
562 |
563 | function makeRequest(requestType as String, parameters as Object, callback as String)
564 | context = createObject("RoSGNode","Node")
565 | if type(parameters)="roAssociativeArray"
566 | context.addFields({parameters: parameters, response: {}})
567 | context.observeField("response", callback) ' response callback is request-specific
568 | if requestType = "url"
569 | m.uriFetcher.request = {context: context}
570 | else if requestType = "registry"
571 | ? "< Accessing Registry for a " parameters.command
572 | m.registryTask.request = {context: context}
573 | end if
574 | end if
575 | end function
576 |
577 | function onKeyEvent(key as String, press as Boolean) as Boolean
578 | handled = false
579 | if press then
580 | if (key = "back") then
581 | handled = false
582 | else
583 | if (key = "options") then
584 | makeRequest("registry" , {section: "", command: "deleteRegistry", key: "", value: "" }, "")
585 |
586 | ' Delete the publisher access token from Roku Cloud
587 | m.clearTokenReq = true
588 | m.store.channelCredData = ""
589 | m.store.command = "storeChannelCredData"
590 |
591 | m.dialogBox.message = "Information: " + chr(10) + "Deleted the sample registry. To repurchase items, please void the transactions at:" + chr(10) + "https://developer.roku.com/developer > Manage Test Users > View (under transactions) > Void transactions"
592 | m.dialogBox.buttons = ["Understood"]
593 | m.top.dialog = m.dialogBox
594 | end if
595 | handled = true
596 | end if
597 | end if
598 | return handled
599 | end function
600 |
--------------------------------------------------------------------------------
/components/MainScene.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
51 |
52 |
63 |
64 |
68 |
74 |
81 |
82 |
83 |
84 |
88 |
89 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/components/RegTask.brs:
--------------------------------------------------------------------------------
1 | '******registry_task*******'
2 | function init()
3 | Registry = CreateObject("roRegistry")
4 | ' RegSec = createObject("roRegistrySection")
5 | m.port = createobject("roMessagePort")
6 | m.top.observefield("request", m.port)
7 | m.top.functionName = "mainThread"
8 | end function
9 |
10 | function mainThread()
11 | while true
12 | msg = wait(0, m.port)
13 | mt = type(msg)
14 | regInput(msg.getData())
15 | end while
16 | end function
17 |
18 | function regInput(context as Object)
19 | context = m.top.request.context
20 | parameters = context.parameters
21 | command = parameters.command
22 | if command = "read"
23 | ? "< Reading key: " parameters.key " in " parameters.section
24 | context.response = {"regVal" : RegRead(parameters.key, parameters.section) }
25 | else if command = "write"
26 | ? "< Writing (" parameters.key " , " parameters.value ") in " parameters.section
27 | RegWrite(parameters.key, parameters.value, parameters.section)
28 | else if command = "delete"
29 | RegDelete(parameters.key, parameters.section)
30 | else if command = "deleteRegistry"
31 | ? "< Deleting the entire sample registry section"
32 | deleteRegistry()
33 | end if
34 | end function
35 |
36 | '******************************************************
37 | 'Registry Helper Functions
38 | '******************************************************
39 | Function RegRead(key as String, section as String) as String
40 | if section = invalid then section = "Default"
41 | sec = CreateObject("roRegistrySection", section)
42 | if sec.Exists(key) then
43 | ? "< Key read is " sec.read(key)
44 | return sec.Read(key)
45 | end if
46 | return "invalid"
47 | End Function
48 |
49 | Function RegWrite(key as String, val as String, section as String) as void
50 | if section = invalid then section = "Default"
51 | sec = CreateObject("roRegistrySection", section)
52 | sec.Write(key, val)
53 | if sec.Flush() then
54 | ? "< Write success!"
55 | ? sec.getKeyList()
56 | else
57 | ? "< Write failed!"
58 | end if
59 |
60 | End Function
61 |
62 | Function RegDelete(key as String, section=invalid) as Void
63 | if section = invalid then section = "Default"
64 | sec = CreateObject("roRegistrySection", section)
65 | sec.Delete(key)
66 | sec.Flush() ' commit it
67 | End Function
68 |
69 | ' use with care
70 | sub deleteRegistry()
71 | ? "Exisiting sections ..."
72 |
73 | reg = CreateObject("roRegistry")
74 | sections = reg.GetSectionList()
75 | exist = false
76 | for each section in sections
77 | ? " detected section " + section
78 | if section = "sample" then exist = true
79 | next
80 | if exist
81 | reg.Delete("sample")
82 | reg.Flush()
83 | ? ""
84 | ? "!-------------------d e l e t e-------------------!"
85 | ? ""
86 | ? "Checking sections ... "
87 | sections = reg.GetSectionList()
88 | for each section in sections
89 | ? " detected section " + section
90 | next
91 | ? "Confirmed delete"
92 | else
93 | ? "Sample section doesn't exist yet, not deleting anything"
94 | end if
95 | ? ""
96 | end sub
97 |
--------------------------------------------------------------------------------
/components/RegTask.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/components/UriFetcher.brs:
--------------------------------------------------------------------------------
1 | function init()
2 | m.port = createObject("roMessagePort")
3 | m.top.observeField("request", m.port)
4 | m.top.observeField("jobsByIdField", "onJobsByIdChanged")
5 | m.top.observeField("urlTransferPoolField", "onTransferPoolChanged")
6 | m.top.functionName = "go"
7 | m.top.control = "RUN"
8 | m.urlTransferPool = [
9 | createObject( "roUrlTransfer" )
10 | createObject( "roUrlTransfer" )
11 | createObject( "roUrlTransfer" )
12 | createObject( "roUrlTransfer" )
13 | createObject( "roUrlTransfer" )
14 | ]
15 | for i = 0 to m.urlTransferPool.count() - 1
16 | t = m.urlTransferPool.getEntry(i)
17 | t.SetCertificatesFile("common:/certs/ca-bundle.crt")
18 | t.AddHeader("X-Roku-Reserved-Dev-Id", "")
19 | t.InitClientCertificates()
20 | end for
21 | m.transferPoolIndex = 0
22 | m.ret = true
23 | end function
24 |
25 | function go() as Void
26 | m.jobsById = {}
27 | m.top.setField("urlTransferPoolField", m.urlTransferPool.count())
28 | while true
29 | msg = wait(0, m.port)
30 | mt = type(msg)
31 | if mt="roSGNodeEvent"
32 | if msg.getField()="request"
33 | m.ret = addRequest(msg.getData())
34 | else
35 | print "> UriFetcher: unrecognized field '"; msg.getField(); "'"
36 | end if
37 | else if mt="roUrlEvent"
38 | processResponse(msg)
39 | else
40 | print "> UriFetcher: unrecognized event type '"; mt; "'"
41 | end if
42 | if m.ret = false
43 | ? "too many requests"
44 | end if
45 | end while
46 | end function
47 |
48 | function addRequest(request as Object) as Boolean
49 | if type(request) = "roAssociativeArray"
50 | context = request.context
51 | if type(context)="roSGNode"
52 | parameters = context.parameters
53 | if type(parameters)="roAssociativeArray"
54 | uri = parameters.uri
55 | if type(uri) = "roString"
56 | m.urlTransferPool.Peek().setUrl(uri)
57 | m.urlTransferPool.Peek().setPort(m.port)
58 |
59 | ' should transfer more stuff from parameters to urlXfer
60 | idKey = stri(m.urlTransferPool.Peek().getIdentity()).trim()
61 | ok = m.urlTransferPool.Peek().AsyncGetToString()
62 | if not ok
63 | m.transferPoolIndex++
64 | if m.urlTransferPool.Count() > m.transferPoolIndex
65 |
66 | ' print "Failed due to: " + m.urlTransferPool.Peek().GetFailureReason()
67 | print "> Using next urlTransfer object in pool"
68 | m.nextFreeObject = m.urlTransferPool.Count()-1 - m.transferPoolIndex
69 | m.urlTransferPool.GetEntry( m.nextFreeObject ).setUrl( uri )
70 | m.urlTransferPool.GetEntry( m.nextFreeObject ).setPort( m.port )
71 | idKey = stri( m.urlTransferPool.GetEntry( m.nextFreeObject ).getIdentity()).trim()
72 | ok = m.urlTransferPool.GetEntry( m.nextFreeObject ).AsyncGetToString()
73 | else
74 | print "> urlTransferPool is fully used"
75 | if not ok
76 | return false
77 | end if
78 | endif
79 | ' print "Resued object in urlTransferPool slot: " + str( m.nextFreeObject )
80 | endif
81 | if ok
82 | m.jobsById[idKey] = {context: context, xfer: m.urlTransferPool}
83 | ' ? "jobsbyID: "; m.jobsbyID.count()
84 | print "> UriFetcher: initiating transfer '"; idkey; "' for URI '"; uri; "'"; " succeeded: "; ok
85 | m.top.setField("JobsByIdField", m.jobsById.count())
86 | else
87 | print "> UriFetcher: invalid uri: "; uri
88 | endif
89 | end if
90 | end if
91 | end if
92 | end if
93 | return true
94 | end function
95 |
96 | function onJobsByIdChanged() as Void
97 | ' transferPoolValue = m.top.getParent().findNode("transferPoolValue")
98 | ' transferPoolValue.text = m.top.getField("jobsByIdField")
99 | end function
100 |
101 | function onTransferPoolChanged() as Void
102 | if m.top.getParent() <> invalid
103 | ' poolIndexValue = m.top.getParent().findNode("poolIndexValue")
104 | ' poolIndexValue.text = m.top.getField("urlTransferPoolField")
105 | end if
106 | end function
107 |
108 | function processResponse(msg as Object)
109 | idKey = stri(msg.GetSourceIdentity()).trim()
110 | job = m.jobsById[idKey]
111 |
112 | ' print "Number of jobs in queue: "; m.jobsById.count()
113 | ' print "Number of urlXfer objects in pool: " m.urlTransferPool.count()
114 |
115 | if job<>invalid
116 | m.transferPoolIndex = 0
117 | m.ret = true
118 | context = job.context
119 | parameters = context.parameters
120 | uri = parameters.uri
121 | ' print "UriFetcher: response for transfer '"; idkey; "' for URI '"; uri; "'"
122 | result = {code: msg.getResponseCode(), content: msg.getString()}
123 | ' could handle various error codes, retry, etc.
124 | m.jobsById.delete(idKey)
125 | m.top.setField("JobsByIdField", m.jobsById.count())
126 | job.context.response = result
127 | else
128 | print "> UriFetcher: event for unknown job "; idkey
129 | end if
130 | end function
131 |
--------------------------------------------------------------------------------
/components/UriFetcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/components/content/ContentScreen.brs:
--------------------------------------------------------------------------------
1 | Sub init()
2 | m.RowList = m.top.findNode("RowList")
3 | m.Title = m.top.findNode("Title")
4 | m.Description = m.top.findNode("Description")
5 | m.Overhang = m.top.findNode("Overhang")
6 | 'm.Poster = m.top.findNode("Poster")
7 | m.RowList.setFocus(true)
8 | m.LoadTask = CreateObject("roSGNode", "FeedParser") 'Create XML parsing node task
9 |
10 | m.LoadTask.observeField("content", "rowListContentChanged")
11 | m.LoadTask.observeField("mediaIndex","indexloaded")
12 | m.LoadTask.control = "RUN" 'Run the task node
13 |
14 | m.RowList.observeField("rowItemFocused", "changeContent")
15 |
16 | m.Video = m.top.findNode("Video")
17 | m.Video.observeField("state", "onVideoStateChanged")
18 | 'm.Video.observeField("position", "onVideoPositionChanged")
19 | m.VideoContent = createObject("roSGNode", "ContentNode")
20 | m.RowList.observeField("rowItemSelected", "playVideo")
21 |
22 | ' so we can use next video in playlist'
23 | m.Video.contentIsPlaylist = true
24 | End Sub
25 |
26 | sub indexloaded(msg as Object)
27 | if type(msg) = "roSGNodeEvent" and msg.getField() = "mediaIndex"
28 | m.mediaIndex = msg.getData()
29 | ? "m.mediaIndex= "; m.mediaIndex
30 | end if
31 | ' signal app launch completed beacon
32 | if m.global.version >= "910"
33 | m.top.setField("beacon", "AppLaunchComplete")
34 | end if
35 | ''? "after set beacon field"
36 | handleDeepLink(m.global.deeplink)
37 | 'get run time deeplink updates'
38 | 'm.global.observeField("deeplink", handleRuntimeDeepLink)
39 | end sub
40 |
41 | Function handleDeepLink(deeplink as object)
42 | if validateDeepLink(deeplink)
43 | playVideo(m.mediaIndex[deeplink.id].url)
44 | else
45 | print "deeplink not validated"
46 | end if
47 | end Function
48 |
49 | function validateDeepLink(deeplink as Object) as Boolean
50 | mediatypes={movie:"movie",episode:"episode",season:"season",series:"series"}
51 | if deeplink <> Invalid
52 | ? "mediaType = "; deeplink.mediaType
53 | ? "contentId = "; deeplink.id
54 | ? "content= "; m.mediaIndex[deeplink.id]
55 | if deeplink.mediaType <> invalid then
56 | if mediatypes[deeplink.mediaType]<> invalid
57 | if m.mediaIndex[deeplink.id] <> invalid
58 | if m.mediaIndex[deeplink.id].url <> invalid
59 | return true
60 | else
61 | print "invalid deep link url"
62 | end if
63 | else
64 | print "bad deep link contentId"
65 | end if
66 | else
67 | print "unknown media type"
68 | end if
69 | else
70 | print "deeplink.type string is invalid"
71 | end if
72 | end if
73 | return false
74 | end function
75 |
76 | Sub rowListContentChanged(msg as Object)
77 | if type(msg) = "roSGNodeEvent" and msg.getField() = "content"
78 | m.RowList.content = msg.getData()
79 | end if
80 | end Sub
81 |
82 | Sub changeContent() 'Changes info to be displayed on the overhang
83 | contentItem = m.RowList.content.getChild(m.RowList.rowItemFocused[0]).getChild(m.RowList.rowItemFocused[1])
84 | 'contentItem is a variable that points to (rowItemFocused[0]) which is the row, and rowItemFocused[1] which is the item index in the row
85 |
86 | 'm.top.getScene().backgroundUri = contentItem.HDPOSTERURL 'Sets Scene background to the image of the focused item
87 | 'm.Poster.uri = contentItem.HDPOSTERURL 'Sets overhang image to the image of the focused item
88 | m.Title.text = contentItem.TITLE 'Sets overhang title to the title of the focused item
89 | m.Description.text = contentItem.DESCRIPTION 'Sets overhang description to the description of the focused item
90 | ''? "row= "; m.RowList.rowItemFocused[0]
91 | ''? "column= "; m.RowList.rowItemFocused[1]
92 | End Sub
93 |
94 | Sub playVideo(url = invalid)
95 | ? "url= "; url
96 | if type(url) = "roSGNodeEvent" ' passed from observe callback'
97 | m.videoContent = m.RowList.content.getChild(m.RowList.rowItemFocused[0])
98 | 'rowItemFocused[0] is the row and rowItemFocused[1] is the item index in the row
99 | else
100 | m.videoContent.url = url
101 | end if
102 |
103 | m.videoContent.streamFormat = "mp4"
104 | keepPlaying = false
105 |
106 | m.Video.content = m.videoContent
107 |
108 | m.Video.visible = "true"
109 | m.Video.control = "play"
110 | column = m.RowList.rowItemFocused[1]
111 | ' avoid double loading bar if it is first item'
112 | if column > 0
113 | m.video.nextContentIndex = column
114 | m.Video.control = "skipcontent"
115 | end if
116 |
117 | m.Video.setFocus(true)
118 |
119 | 'm.vector2danimation = m.top.FindNode("moveOverhangPanelUp")
120 | 'm.vector2danimation.repeat = false
121 | 'm.vector2danimation.control = "start"
122 | m.Overhang.visible = false
123 | End Sub
124 |
125 | Function returnToUIPage()
126 | ? "m.Video.pauseBufferStart= "; m.Video.pauseBufferStart
127 | m.Video.visible = "false" 'Hide video
128 | m.Video.control = "stop" 'Stop video from playing
129 | m.RowList.setFocus(true)
130 |
131 | 'm.vector2danimation = m.top.FindNode("moveOverhangPanelDown")
132 | 'm.vector2danimation.repeat = false
133 | 'm.vector2danimation.control = "start"
134 | m.Overhang.visible = true
135 | end Function
136 |
137 | Function onVideoStateChanged(msg as Object)
138 | if type(msg) = "roSGNodeEvent" and msg.getField() = "state"
139 | state = msg.getData()
140 | ? "onVideoStateChanged() state= "; state
141 | if state = "finished"
142 | returnToUIPage()
143 | end if
144 | end if
145 | end Function
146 |
147 | Function onVideoPositionChanged(msg as Object)
148 | if type(msg) = "roSGNodeEvent" and msg.getField() = "position"
149 | position = msg.getData()
150 | ''? "contentIndex = "; m.video.contentIndex
151 | ''? "nextcontentIndex= "; m.video.nextcontentindex
152 | ''? "onVideoPositionChanged() position= "; position
153 | end if
154 | end Function
155 |
156 | Function onKeyEvent(key as String, press as Boolean) as Boolean 'Maps back button to leave video
157 | if press
158 | print "pressed key="; key
159 | if key = "back" 'If the back button is pressed
160 | if m.Video.visible
161 | returnToUIPage()
162 | return true
163 | else
164 | return false
165 | end if
166 | end if
167 | end if
168 | end Function
169 |
--------------------------------------------------------------------------------
/components/content/ContentScreen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
22 |
23 |
24 |
37 |
38 |
45 |
46 |
47 |
52 |
53 |
54 |
55 |
56 |
61 |
62 |
69 |
70 |
79 |
80 |
81 |
82 |
86 |
87 |
91 |
92 |
93 |
94 |
98 |
99 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/components/content/FeedParser.brs:
--------------------------------------------------------------------------------
1 | Function ParseXML(str As String) As dynamic 'Takes in the content feed as a string
2 | if str = invalid return invalid 'if the response is invalid, return invalid
3 | xml = CreateObject("roXMLElement") '
4 | if not xml.Parse(str) return invalid 'if the string cannot be parse, return invalid
5 | return xml 'returns parsed XML if not invalid
6 | End Function
7 |
8 |
9 | Function GetContentFeed() 'This function retrieves and parses the feed and stores the content item in a ContentNode
10 | url = CreateObject("roUrlTransfer") 'component used to transfer data to/from remote servers
11 | url.SetUrl("https://devtools.web.roku.com/samples/sample_content.rss")
12 | url.SetCertificatesFile("common:/certs/ca-bundle.crt")
13 | rsp = url.GetToString() 'convert response into a string
14 |
15 | responseXML = ParseXML(rsp) 'Roku includes its own XML parsing method
16 |
17 | if responseXML <> invalid then 'Fall back in case Roku's built in XML parse method fails
18 | responseXML = responseXML.GetChildElements() 'Access content inside Feed
19 | responseArray = responseXML.GetChildElements()
20 | End if
21 |
22 | 'manually parse feed if ParseXML() is invalid
23 | result = [] 'Store all results inside an array. Each element respresents a row inside our RowList stored as an Associative Array (line 63)
24 |
25 | for each xmlItem in responseArray 'For loop to grab contents inside each item in XML feed
26 | if xmlItem.getName() = "item" 'Each individual channel content is stored inside the XML header named
27 | itemAA = xmlItem.getChildElements() 'Get the child elements of item
28 | if itemAA <> invalid 'Fall bak in case invalid is returned
29 | item = {} 'Creates an associative array for each row
30 | for each xmlItem in itemAA ' Goes thru all contents of itemAA
31 | item[xmlItem.getName()] = xmlItem.getText()
32 | if xmlItem.getName() = "media:content" 'Checks to find header
33 | item.stream = {url: xmlItem.url} ' Assigns all content inside to the item AA
34 | item.url = xmlItem.getAttributes().url
35 | item.streamFormat = "mp4"
36 |
37 | mediaContent = xmlItem.GetChildElements()
38 | for each mediaContentItem in mediaContent 'Looks through meiaContent to find poster image for each piece of content
39 | if mediaContentItem.getName() = "media:thumbnail"
40 | item.HDPosterURL = mediaContentItem.getattributes().url 'Assign image to item AA
41 | item.HDBackgroundImageUrl = mediaContentItem.getattributes().url
42 | end if
43 | end for
44 | ''? "contentId= "; xmlItem.getText()
45 |
46 | end if
47 | end for
48 | result.push(item) 'Pushes each AA into the Array
49 | 'STOP
50 | end if
51 | end if
52 | end for
53 | return result 'Returns the array
54 | End Function
55 |
56 |
57 | Function ParseXMLContent(list As Object) 'Formats content into content nodes so they can be passed into the RowList
58 | RowItems = createObject("RoSGNode","ContentNode")
59 | 'Content node format for RowList: ContentNode(RowList content) ---> ContentNodes for each row ---> ContentNodes for each item in the row)
60 | for each rowAA in list
61 | row = createObject("RoSGNode","ContentNode")
62 | row.Title = rowAA.Title
63 |
64 | for each itemAA in rowAA.ContentList
65 | item = createObject("RoSGNode","ContentNode")
66 | 'Don't do item.SetFields(itemAA), as it doesn't cast streamFormat to proper value
67 | 'for each key in itemAA
68 | ' ?"key = ", key, itemAA[key]
69 | 'item[key] = itemAA[key]
70 | 'end for
71 | item.setFields(itemAA)
72 | row.appendChild(item)
73 | end for
74 | RowItems.appendChild(row)
75 | end for
76 | return RowItems
77 | End Function
78 |
79 | Sub Init()
80 | m.top.functionName = "loadContent"
81 | End Sub
82 |
83 | Function SelectTo(array as Object, num=25 as Integer, start=0 as Integer) as Object 'This method copies an array up to the defined number 'num' (default 25)
84 | result = []
85 | for i = start to array.count()-1
86 | result.push(array[i])
87 | if result.Count() >= num
88 | exit for
89 | end if
90 | end for
91 | return result
92 | End Function
93 |
94 | Sub loadContent()
95 | oneRow = GetContentFeed()
96 | list = [
97 | 'first row in the grid with 3 items across
98 | {
99 | Title:"Row One"
100 | ContentList: SelectTo(oneRow, 3)
101 | }
102 | 'second row in the grid with 5 items across
103 | {
104 | Title:"Row Two"
105 | ContentList: SelectTo(oneRow, 5, 3)
106 | }
107 | 'third row in the grid with 5 items across
108 | {
109 | Title:"Row Three"
110 | ContentList: SelectTo(oneRow, 5, 8)
111 | }
112 | 'fourth row in the grid with remaining 2 items
113 | {
114 | Title:"Row Four"
115 | ContentList: SelectTo(oneRow, 5, 13)
116 | }
117 | ]
118 |
119 | m.top.content = ParseXMLContent(list)
120 | End Sub
121 |
--------------------------------------------------------------------------------
/components/content/FeedParser.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/components/content/PosterItem.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/components/screens/SignInScreen.brs:
--------------------------------------------------------------------------------
1 | sub init()
2 | m.top.observeField("setup", "setupSignInPage")
3 | m.signInButton = m.top.findNode("signInButton")
4 | m.emailField = m.top.findNode("signinEmail")
5 | m.passwordField = m.top.findNode("signinPassword")
6 | m.signInTips = m.top.findNode("signInTips")
7 | m.keyboardDialog = m.top.findNode("signinKeyboard")
8 | m.keyboardDialog.observeFieldScoped("buttonSelected", "dismissDialog")
9 | m.keyboardDialog.observeFieldScoped("text", "handleTextEdit")
10 |
11 | 'm.doneButton = m.top.findNode("signinDoneButton")
12 |
13 | m.signInButton.observeField("buttonSelected","onSignInButtonSelected")
14 |
15 | ' initialize selectedObj to password'
16 | m.selectedObj = "password"
17 | end sub
18 |
19 | sub setupSignInPage(msg)
20 | ? "call setupSignInPage()"
21 | m.emailField.text = m.top.email
22 | if m.emailField.text = ""
23 | m.selectedObj = "email"
24 | m.signInTips.text = "Press OK to enter email, then press Down key"
25 | m.emailField.setFocus(true)
26 | else
27 | m.passwordField.setFocus(true)
28 | end if
29 | end sub
30 |
31 | sub setUpEditEmail()
32 | m.keyboardDialog.textEditBox.secureMode = false
33 | m.keyboardDialog.keyboardDomain = "email"
34 | m.keyboardDialog.title = "Email entry"
35 | m.keyboardDialog.message = ["It is easier to share email using RFI"]
36 | m.keyboardDialog.buttons = ["OK"]
37 | m.keyboardDialog.textEditBox.hintText = "Enter a valid email address..."
38 | m.keyboardDialog.text = m.emailField.text
39 | end sub
40 |
41 | sub setupEditPassword()
42 | m.keyboardDialog.keyboardDomain = "password"
43 | 'm.top.textEditBox.voiceEntryType = "password"
44 | m.keyboardDialog.textEditBox.secureMode = true
45 |
46 | m.keyboardDialog.title = "Password entry"
47 | m.keyboardDialog.message = ["The password is 8 or more characters"]
48 | m.keyboardDialog.buttons = ["OK"]
49 | m.keyboardDialog.textEditBox.hintText = "Create or enter a password..."
50 | m.keyboardDialog.text = m.passwordField.text
51 | end sub
52 |
53 | sub handleTextEdit(msg)
54 | if m.selectedObj = "password"
55 | m.passwordField.text = m.keyboardDialog.text
56 | else if m.selectedObj = "email"
57 | m.emailField.text = m.keyboardDialog.text
58 | end if
59 | end sub
60 |
61 | sub dismissDialog()
62 | print "called dismissDialog"
63 | m.keyboardDialog.close=true
64 | 'Revert focus'
65 | if m.selectedObj = "password"
66 | m.passwordField.setFocus(true)
67 | else if m.selectedObj = "email"
68 | m.emailField.setFocus(true)
69 | end if
70 | m.keyboardDialog.visible=false
71 | end sub
72 |
73 | function onSignInButtonSelected()
74 | ' return to main scene and check email/password'
75 | hashedPass = hashThePassword(m.passwordField.text)
76 | responseAA = {type:"signin", email:m.emailField.text, password:hashedPass}
77 | returnToMainScene(responseAA)
78 | end function
79 |
80 | sub returnToMainScene(ret)
81 | scene = m.top.getScene()
82 | scene.response = ret ' responseAA'
83 | m.top.visible = false
84 | end sub
85 |
86 | function onKeyEvent(key as String, press as Boolean) as Boolean
87 | handled = false
88 | ? "signinpage key= "; key; " press= "; press
89 | if press then
90 | if key = "back" then
91 | returnToMainScene("")
92 | else if key = "down"
93 | if m.selectedObj = "email"
94 | m.emailField.active=false
95 | m.passwordField.setFocus(true)
96 | m.passwordField.active=true
97 | m.selectedObj = "password"
98 | m.signInTips.text = "Press OK to enter password, then press Down key"
99 | else if m.selectedObj = "password"
100 | m.passwordField.active=false
101 | m.signInButton.setFocus(true)
102 | m.selectedObj = "button"
103 | m.signInTips.text = "Press OK to sign in"
104 | end if
105 | print "selectedObj= "; m.selectedObj
106 | else if key = "up"
107 | if m.selectedObj = "password"
108 | m.passwordField.active=false
109 | m.emailField.setFocus(true)
110 | m.emailField.active=true
111 | m.selectedObj = "email"
112 | m.signInTips.text = "Press OK to enter email, then press Down key"
113 | else if m.selectedObj = "button"
114 | m.passwordField.setFocus(true)
115 | m.passwordField.active=true
116 | m.selectedObj = "password"
117 | m.signInTips.text = "Press OK to enter password, then press Down key"
118 | end if
119 | print "selectedObj= "; m.selectedObj
120 | else if key = "OK"
121 | if m.selectedObj = "password"
122 | setupEditPassword()
123 | else if m.selectedObj = "email"
124 | setupEditEmail()
125 | end if
126 | m.keyboardDialog.visible=true
127 | m.keyboardDialog.setFocus(true)
128 | print "selectedObj= "; m.selectedObj
129 | end if
130 | end if
131 | return handled
132 | end function
133 |
--------------------------------------------------------------------------------
/components/screens/SignInScreen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
22 |
29 |
36 |
44 |
47 |
49 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/components/screens/SignUpScreen.brs:
--------------------------------------------------------------------------------
1 | sub init()
2 | m.top.observeField("setup", "setupSignUpPage")
3 | m.signUpButton = m.top.findNode("signUpButton")
4 | m.emailField = m.top.findNode("signupEmail")
5 | m.keyboardDialog = m.top.findNode("signupKeyboard")
6 | m.keyboardDialog.observeFieldScoped("buttonSelected", "dismissDialog")
7 | m.keyboardDialog.observeFieldScoped("text", "handleTextEdit")
8 |
9 | m.signUpButton.observeField("buttonSelected","onSignUpButtonSelected")
10 |
11 | ' initialize selectedObj to email'
12 | m.selectedObj = "email"
13 | end sub
14 |
15 | sub setupSignUpPage(msg)
16 | ? "call setupSignUpPage()"
17 | m.emailField.text = m.top.email
18 | m.selectedObj = "email"
19 | m.emailField.setFocus(true)
20 | end sub
21 |
22 | sub setUpEditEmail()
23 | m.keyboardDialog.textEditBox.secureMode = false
24 | m.keyboardDialog.keyboardDomain = "email"
25 | m.keyboardDialog.title = "Email entry"
26 | m.keyboardDialog.message = ["It is easier to share email using RFI"]
27 | m.keyboardDialog.buttons = ["OK"]
28 | m.keyboardDialog.textEditBox.hintText = "Enter a valid email address..."
29 | m.keyboardDialog.text = m.emailField.text
30 | end sub
31 |
32 | sub handleTextEdit(msg)
33 | m.emailField.text = m.keyboardDialog.text
34 | end sub
35 |
36 | sub dismissDialog()
37 | print "called dismissDialog"
38 | m.keyboardDialog.close=true
39 | 'Revert focus'
40 | m.emailField.setFocus(true)
41 | m.keyboardDialog.visible=false
42 | end sub
43 |
44 | function onSignUpButtonSelected()
45 | ' return to main scene and check email/password'
46 | hashedPass = hashThePassword("")
47 | responseAA = {type:"signup", email:m.emailField.text, password:hashedPass}
48 | returnToMainScene(responseAA)
49 | end function
50 |
51 | sub returnToMainScene(ret)
52 | scene = m.top.getScene()
53 | scene.response = ret ' responseAA'
54 | m.top.visible = false
55 | end sub
56 |
57 | function onKeyEvent(key as String, press as Boolean) as Boolean
58 | handled = false
59 | ? "signuppage key= "; key; " press= "; press
60 | if press then
61 | if key = "back" then
62 | returnToMainScene("")
63 | else if key = "down"
64 | if m.selectedObj = "email"
65 | m.emailField.active=false
66 | m.signUpButton.setFocus(true)
67 | m.selectedObj = "button"
68 | end if
69 | print "selectedObj= "; m.selectedObj
70 | else if key = "up"
71 | if m.selectedObj = "button"
72 | m.emailField.active=true
73 | m.emailField.setFocus(true)
74 | m.selectedObj = "email"
75 | end if
76 | print "selectedObj= "; m.selectedObj
77 | else if key = "OK"
78 | if m.selectedObj = "email"
79 | setupEditEmail()
80 | end if
81 | m.keyboardDialog.visible=true
82 | m.keyboardDialog.setFocus(true)
83 | print "selectedObj= "; m.selectedObj
84 | end if
85 | end if
86 | return handled
87 | end function
88 |
--------------------------------------------------------------------------------
/components/screens/SignUpScreen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
28 |
35 |
38 |
40 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/images/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rokudev/on-device-authentication/75a0c69a7b68e00c466b1bef43e9d2f7710a5c17/images/background.jpg
--------------------------------------------------------------------------------
/images/icon_focus_hd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rokudev/on-device-authentication/75a0c69a7b68e00c466b1bef43e9d2f7710a5c17/images/icon_focus_hd.jpg
--------------------------------------------------------------------------------
/images/purchased_poster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rokudev/on-device-authentication/75a0c69a7b68e00c466b1bef43e9d2f7710a5c17/images/purchased_poster.png
--------------------------------------------------------------------------------
/images/roku-developers-top.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rokudev/on-device-authentication/75a0c69a7b68e00c466b1bef43e9d2f7710a5c17/images/roku-developers-top.jpg
--------------------------------------------------------------------------------
/images/splash_hd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rokudev/on-device-authentication/75a0c69a7b68e00c466b1bef43e9d2f7710a5c17/images/splash_hd.jpg
--------------------------------------------------------------------------------
/manifest:
--------------------------------------------------------------------------------
1 | title= On Device Authentication Sample
2 | subtitle=A demo on authenticating purchases on device
3 | major_version=1
4 | minor_version=2
5 | build_version=00001
6 |
7 | mm_icon_focus_hd=pkg:/images/icon_focus_hd.jpg
8 | splash_screen_hd=pkg:/images/splash_hd.jpg
9 | splash_color=#000000
10 | splash_min_time=1000
11 |
12 | # sampleHardCodedValues should be set to false if making
13 | # own query to own server, this value is just for
14 | # the purpose of this sample
15 |
16 | bs_const=sampleHardCodedValues=True
17 |
--------------------------------------------------------------------------------
/rasp/OnDeviceAuthenticationSignIn.rasp:
--------------------------------------------------------------------------------
1 | params:
2 | rasp_version: 1
3 | default_keypress_wait: 2
4 | channels:
5 | On-device authentication: dev
6 | steps:
7 | - launch: On-device authentication
8 | - press: down
9 | - press: ok
10 | - press: down
11 | - press: ok
12 | - press: ok
13 | - text: script-login
14 | - press: down
15 | - press: down
16 | - press: down
17 | - press: down
18 | - press: ok
19 | - press: down
20 | - press: ok
21 | - text: script-password
22 | - press: down
23 | - press: ok
24 | - press: down
25 | - press: ok
--------------------------------------------------------------------------------
/rasp/OnDeviceAuthenticationSignOut.rasp:
--------------------------------------------------------------------------------
1 | params:
2 | rasp_version: 1
3 | default_keypress_wait: 2
4 | channels:
5 | On-device authentication: dev
6 | steps:
7 | - launch: On-device authentication
8 | - press: info
9 | - press: ok
10 | - press: back
--------------------------------------------------------------------------------
/source/main.brs:
--------------------------------------------------------------------------------
1 | '********** Copyright 2016 Roku Corp. All Rights Reserved. **********
2 |
3 | sub Main()
4 | screen = CreateObject("roSGScreen")
5 | m.port = CreateObject("roMessagePort")
6 | screen.setMessagePort(m.port)
7 | scene = screen.CreateScene("MainScene")
8 | screen.show()
9 | m.global = screen.getGlobalNode()
10 |
11 | ' m.global.addField("config", "assocarray", false)
12 | ' m.global.config = {
13 | ' publisherEntitlement: true,
14 | ' publisherTokenKey: "8ZQEDDR8AWVJF8AH",
15 | ' publisherRefreshToken: "MSEFAJ7A54SE3LBE",
16 | ' publisherEndPoint: "sample.com/endpoint/1234",
17 | ' }
18 |
19 | while(true)
20 | msg = wait(0, m.port)
21 | msgType = type(msg)
22 | if msgType = "roSGScreenEvent"
23 | if msg.isScreenClosed() then return
24 | end if
25 | end while
26 | end sub
27 |
--------------------------------------------------------------------------------
/source/utils.brs:
--------------------------------------------------------------------------------
1 |
2 | ' NOTE: this is not a Roku recommended way to hash a password'
3 | ' it is just a demonstration of hashing in the sign up or sign in flow'
4 | function hashThePassword(password) as String
5 | ba = CreateObject("roByteArray")
6 | passWithSalt = password + "THESALT"
7 | ba.FromAsciiString(passWithSalt)
8 | digest = CreateObject("roEVPDigest")
9 | digest.Setup("sha256")
10 | result = digest.Process(ba)
11 | return result
12 | end function
13 |
14 |
--------------------------------------------------------------------------------