README
@dopt/javascript / Exports
Dopt JavaScript SDK
Getting started
The Dopt JavaScript SDK offers a convenient way to accessing, update, and subscribe to objects exposed via Dopt's blocks and flows APIs. You can use this SDK to bind user flow state (defined in Dopt) to your UI.
The SDK lives in our open-source monorepo odopt.
It is published to npm as @dopt/javascript
.
Check out our TypeDoc docs for source code level documentation.
Installation
Via npm:
npm install @dopt/javascript
Via Yarn:
yarn add @dopt/javascript
Via pnpm:
pnpm add @dopt/javascript
Configuration
To initialize the SDK, you will need:
- A blocks API key (generated in Dopt)
- The flow identifiers and version tags for the flows you want your end-users to experience
- A user ID (user being an end-user you've identified to Dopt)
Usage
Initialization
You can initialize Dopt in your app as follows:
const dopt = new Dopt({
apiKey,
userId,
flowVersions: { "welcome-to-dopt": 3 },
});
Flows and blocks
The SDK gives you access to two related objects: flows and blocks. Flows are entities representing the flow you designed in Dopt. Blocks are a subset of the blocks in that flow.
Flow objects available through the SDK are represented by the following type definition:
interface Flow<T = "flow"> {
readonly kind: "flow";
readonly type: T;
readonly uid: string;
readonly sid: string;
readonly version: number;
readonly state: {
started: boolean;
completed: boolean;
exited: boolean;
};
blocks: Block[];
}
The states of a flow are 1:1 with the actions you can perform on a flow. Flows have blocks, which are represented through the following type definition:
interface Step {
readonly kind: "block";
readonly type: "model";
readonly uid: string;
readonly sid: string;
readonly version: number;
readonly state: {
active: boolean;
completed: boolean;
};
}
interface Group {
readonly kind: "block";
readonly type: "set";
readonly uid: string;
readonly sid: string;
readonly version: number;
readonly state: {
active: boolean;
completed: boolean;
};
length: number;
blocks: Step[];
ordered: boolean;
}
type Block = Group | Step;
Unlike flows, the states of a block are not all 1:1 with actions you can perform. The completed
state does have an associated action, but the active
state is special.
Key concept: The active
state of a block is controlled by Dopt and represents where the initialized user (specified by the userId
prop) is in the flow. As you or other actors perform actions that implicitly transition the user through the Flow, the active
state is updated.
Accessing flows and blocks
Now that you know what objects are available through the SDK, let's talk about how you access them.
You can use the blocks()
method to access all blocks associated with the flowVersions
specified to the SDK.
const blocks = dopt.blocks();
blocks.forEach((block) => console.log(block));
You can access individual blocks via the block(identifier: string)
method:
const block = dopt.block("HNWvcT78tyTwygnbzU6SW");
console.log(
"I'm on particular block in version 3 of the 'welcome-to-dopt' flow",
block
);
We also expose flow accessors. You can use the flows()
method to access all flows associated with the flowVersions
specified to the SDK.
const flows = dopt.flows();
flows.forEach((flow) => console.log(flow));
Additionally, you can access individual flows via the flow(uid: string, version: number)
method:
const flow = dopt.flow("welcome-to-dopt");
console.log("I'm version 3 of the `welcome-to-dopt` flow", flow);
The dopt
object exposes an initialized
method which you can use to guard calls to any block accessors:
dopt.initialized().then(() => {
// Safely access block(s) or flow(s)!
const blocks = dopt.blocks();
const block = dopt.block("HNWvcT78tyTwygnbzU6SW");
});
Subscribing to flow or block state change
You can use the subscribe()
method on the flow and block classes to listen for changes to then underlying object:
const block = dopt.block("HNWvcT78tyTwygnbzU6SW");
block.subscribe((block: BlockType) =>
// The block passed to the listener is a data object.
// To access the full class, use `dopt.block(block.uid)`.
console.log(`Block ${block.uid} has updated`, block)
);
const flow = dopt.flow("welcome-to-dopt");
flow.subscribe((flow: FlowType) =>
// The flow passed to the listener is a data object.
// To access the full class, use `dopt.flow(flow.uid)`.
console.log(`Flow ${flow.uid} has updated`, flow)
);
Using intentions to trigger flow and block state changes
Our flow and block classes provide intention methods which you can use to progress and update their state. For example, when a specific step in your onboarding flow is complete, you can call block.complete()
to mark that step as done.
These methods, like flow.complete()
or block.complete()
are defined with signatures that explicitly do not return values: () => void
. We do this because each intention may cause a flow and / or block transition along with other side effects. These changes will eventually propagate back to the client. Then the client will reactive update and re-render components based on the subscriptions you've defined via flow.subscribe(...)
and block.subscribe(...)
. Calling an intention only means that at sometime in the future, the client's state will be updated.
Understanding loading status
We expose two functions which enable you to wait for Dopt to initialize, both within the larger Dopt
provider class and at the granular Flow
class level. To wait for all of Dopt to initialize, you can use the dopt.initialized()
function on an instance of the Dopt
class. This function returns a promise which resolves after Dopt has completed loading.
If you would instead like to wait for specific flows, you can use the flow.initialized()
function on an instance of the Flow
class. This function returns a promise which resolves after that specific flow has completed loading; additionally, the promise will resolve to true
if the loading was successful and false
otherwise.
Example usage
import { NewUserOnboarding } from "@/onboarding/new-user";
const dopt = new Dopt({
apiKey,
userId,
flowVersions: { "welcome-to-dopt": 3 },
});
dopt.initialized().then(() => {
const userOnboardingModal = new NewUserOnboardingModal();
const block = dopt.block("HNWvcT78tyTwygnbzU6SW");
// subscribe to changes in your blocks's state
// you can also unsubscribe the listener by calling the returned function
const unsubscribe = block.subscribe(({ active }: BlockType) => {
if (!active) {
userOnboardingModal.hide();
} else {
userOnboardingModal.render().show();
}
});
// initially render your component, if it's active
if (block.state().active) {
userOnboardingModal.render().show();
// complete the block where appropriate
userOnboardingModel.on("done", block.complete);
}
});
Debugging
The SDK accepts a logLevel
parameter that allows you to set the minimum log level you would like to print into the console. This defaults to 'silent'
.
const dopt = new Dopt({
apiKey,
userId,
logLevel: "warn", // 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent'
flowVersions: { "welcome-to-dopt": 3 },
});
Optimistic updates
DoptConfig
and Dopt
also accept a optimisticUpdates
(boolean
) prop that will optimistically update the state of a block when the complete intent method is called. This defaults to true
. As of right now, only a step block's complete
intent can be optimistically updated.
Feedback
Looking to provide feedback or report a bug? Open an issue or contact us at support@dopt.com.
Contributing
All contributions are welcome! Feel free to open a pull request.