Skip to content

Commit

Permalink
Plugins android clean up (#290)
Browse files Browse the repository at this point in the history
* beacon

* check-path

* coroutines + usage for check-path

* expression

* external-action

* metrics

* fix a bunch of formatting things

* fix formatting problems

* settimeout

* async-node

* update package paths

* async node ios docs

* address some comments

---------

Co-authored-by: brocollie08 <[email protected]>
Co-authored-by: nancywu1 <[email protected]>
  • Loading branch information
3 people authored Feb 2, 2024
1 parent 87d1ac3 commit e759140
Show file tree
Hide file tree
Showing 14 changed files with 426 additions and 37 deletions.
2 changes: 1 addition & 1 deletion android/demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ bazel run //android:demo
```

```
bazel mobile-install //android/demo
bazel run //android/demo:install
```

If those command do not run, you can find the apk in `bazelbin/android/demo/install.runfiles/player/android/demo/demo.apk` and drag this apk onto the emulated device. This will install it. ( you may need to swipe on the device to see the application)
Expand Down
196 changes: 196 additions & 0 deletions docs/site/pages/plugins/async-node.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
---
title: AsyncNode Plugin
platform: core,react,ios,android
---

# Async Node Plugin

The AsyncNode Plugin is used to enable streaming additional content into a flow that has already been loaded and rendered.
A common use case for this plugin is conversational UI, as the users input more dialogue, new content must be streamed into Player in order to keep the UI up to date.

The pillar that makes this possible is the concept of an `AsyncNode`. An `AsyncNode` is any tree node with the property `async: true`, it represents a placeholder node that will be replaced by a concrete node in the future.

In the example below, node with the id "some-async-node" will not be rendered on first render, but will be replaced with a UI asset node at a later time:

```json
{
"id": "flow-1",
"views": [
{
"id": 'action',
"actions": [
{
"id": "some-async-node",
"async": true,
},
],
},
],
...
}
```

The `AsyncNodePlugin` exposes an `onAsyncNode` hook on all platforms. The `onAsyncNode` hook will be invoked with the current node when the plugin is available and an `AsyncNode` is detected during the resolve process. The node used to call the hook with could contain metadata according to content spec.

User should tap into the `onAsyncNode` hook to examine the node's metadata before making a decision on what to replace the async node with. The return could be a single asset node or an array of asset nodes.


### Continuous Streaming

In order to keep streaming in new content, there must be at least 1 or more `AsyncNode`s in the view tree at all times.
This means there must be a constant renewal of new `AsyncNode`s after the previous ones are resolved by the user.

## Usage

<PlatformTabs>
<core>

Add the plugin to Player:

```js
import { Player } from '@player-ui/player';
import { AsyncNodePlugin } from '@player-ui/async-node-plugin';

const asyncNodePlugin = new AsyncNodePlugin();

// Configuring async node behaviour
asyncNodePlugin.hooks.onAsyncNode.tap('handleAsync', async (node: Node.Node) => {
...
// Determine what to return to be parsed into a concrete UI asset
});

const player = new Player({
plugins: [
asyncNodePlugin
]
})
```
</core>
<react>

The `react` version of the AsyncNodePlugin is identical to using the core plugin. Refer to core usage for handler configuration:

```js
import { ReactPlayer } from '@player-ui/react';
import { MetricsPlugin } from '@player-ui/async-node-plugin';

const asyncNodePlugin = new AsyncNodePlugin();

const player = new ReactPlayer({
plugins: [
asyncNodePlugin
]
})
```

</react>
<ios>

### CocoaPods

Add the subspec to your `Podfile`

```ruby
pod 'PlayerUI/AsyncNodePlugin'
```

### Swift Usage

In integration code

```swift
var body: some View {
SwiftUIPlayer(
flow: flow,
plugins: [
AsyncNodePlugin { node in
// Determine what to return either using the singleNode or multiNode case
// Then JSON can be provided using the concrete case, see below for using the encodable case
return .singleNode(.concrete(jsContext?.evaluateScript("""
({"asset": {"id": "text", "type": "text", "value":"new node from the hook"}})
""") ?? JSValue()))

// OR
return .multiNode([
.concrete(jsContext?.evaluateScript("""
({"asset": {"id": "text", "type": "text", "value":"1st value in the multinode"}})
""") ?? JSValue()),
.concrete(jsContext?.evaluateScript("""
({"asset": {"id": "async-node-2", "async": "true" }})
""") ?? JSValue())
])

}
],
result: $resultBinding
)
}
```

The plugin also provides a default asset placeholder struct that is encodable, instead of passing in the JSON string users can use
`AssetPlaceholderNode` which includes an `asset` key that takes any user defined Encodable struct as the value. Assuming the following encodable struct is defined:

```swift
struct PlaceholderNode: Codable, Equatable, AssetData {
public var id: String
public var type: String
var value: String?

public init(id: String, type: String, value: String? = nil) {
self.id = id
self.type = type
self.value = value
}
}
```

Instead of using the JSON string above, the following can be used:

```swift
return .singleNode(.encodable(PlaceholderNode(id: "text", type: "text", value: "new node from the hook")))

// OR

return .multiNode([
ReplacementNode.encodable(PlaceholderNode(id: "text", type: "text", value: "1st value in the multinode")),
ReplacementNode.encodable(AsyncNode(id: "id"))])
```

Note: the AsyncNode struct is already defined in the plugin with the `async` property defaulted to true so only `id` needs to be passed in

As a convenience to the user, the AsyncNodePlugin just takes a callback which has the content to be returned, this is provided to the plugin which calls the the `onAsyncNode` hook tap method

</ios>
<android>

In build.gradle
```kotlin
implementation "com.intuit.player.plugins:async-node:$PLAYER_VERSION"
```

In integration code
```kotlin
import com.intuit.player.plugins.asyncnode.AsyncNodePlugin

val asyncNodePlugin = AsyncNodePlugin()

// Configuring async node behaviour
asyncNodePlugin.hooks.onAsyncNode.tap("handleAsync") { hookContext, node ->
...
// Determine what to return in the form of a list of maps representing UI asset to be parsed
// e.g.
// listOf(
// mapOf(
// "asset" to mapOf(
// "id" to "asset-1",
// "type" to "text",
// "value" to "new asset!"
// )
// )
// )
}

AndroidPlayer(asyncNodePlugin)
```
</android>
</PlatformTabs>
45 changes: 39 additions & 6 deletions docs/site/pages/plugins/beacon.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,12 @@ For convenience, there are several extension methods for utilizing a pre-install

```kotlin
// registering handler
player.onBeacon { beacon: String ->
androidPlayer.onBeacon { beacon: String ->
// Process beacon and send to analytics platform
}

// fire beacon
player.beacon(
androidPlayer.beacon(
"clicked",
"button",
assetThatIsBeaconing,
Expand All @@ -201,10 +201,7 @@ player.beacon(

The base `RenderableAsset` class provides an additional helper to make beaconing less verbose from an asset perspective.

```kotlin
// within RenderableAsset implementation
beacon(BeaconAction.clicked, BeaconElement.button)
```
[Helper in RenderableAsset](https://github.com/player-ui/player/blob/2a2110caf87ab752207c82ec9ab8ce72316d20f5/android/player/src/main/java/com/intuit/player/android/asset/RenderableAsset.kt#L253)

</android>
</PlatformTabs>
Expand Down Expand Up @@ -288,4 +285,40 @@ struct ActionAssetView: View {
```

</ios>

<android>

Using the base `RenderableAsset`'s helper function in this example fires off a basic beacon with no adjustment to format as well as empty data upon button click

**Example**

```kotlin
class MyAsset(assetContext: AssetContext) :
DecodableAsset<>(assetContext, Data.serializer()) {

@Serializable
data class Data(
...
) : AssetData()

override fun createView(): View {
...
}

override fun View.hydrate() {
val binding = MyCustomViewBinding.bind(this)
binding.button.setOnClickListener {
this@MyAsset.beacon(
action = BeaconAction.clicked,
element = BeaconElement.button,
asset = this@MyAsset,
data = null
)
}
...
}
}
```

</android>
</PlatformTabs>
30 changes: 28 additions & 2 deletions docs/site/pages/plugins/check-path.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Check Path Plugin
platform: react,core
platform: react,core,android
---

# Check Path Plugin
Expand Down Expand Up @@ -48,7 +48,7 @@ Install the plugin:
```bash
yarn add @player-ui/check-path-plugin-react
```

6474792156
Add it to Player:

```js
Expand Down Expand Up @@ -91,6 +91,23 @@ var body: some View {
```
</ios>

<android>
In build.gradle

```kotlin
implementation "com.intuit.player.plugins:check-path:$PLAYER_VERSION"
```

In Player constructor

```kotlin
import com.intuit.player.plugins.checkpath.CheckPathPlugin

val plugins = listOf(CheckPathPlugin())
AndroidPlayer(plugins)
```
</android>

</PlatformTabs>


Expand Down Expand Up @@ -201,6 +218,15 @@ struct MyAsset: View {
```
</ios>

<android>

The JVM CheckPathPlugin API contains partial functionality in comparison to the core plugin, ideally this should not be used, and all logic where pathing is concerned should be handled in transforms
If the situation arises and this plugin is absolutely necessary, the API from the following source are available at the moment. This is subject to change in future releases:

[CheckPathPlugin](https://github.com/player-ui/player/blob/main/plugins/check-path/jvm/src/main/kotlin/CheckPathPlugin.kt)

</android>

</PlatformTabs>


Loading

0 comments on commit e759140

Please sign in to comment.