├── 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 | 46 | 47 | 50 | 51 | 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 | 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 |