From d0f96fd993f7ff90e419adb62b24c6b2b2a719bd Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 24 Aug 2022 13:42:06 -0500 Subject: [PATCH 1/2] Add ctx.redirect() method This currently "clears" the pending redirect as soon as it's sent to a single client, in order to potentially facilitate returning to the transaction afterward (maybe as part of an OAuth flow, for example). That doesn't currently actually work very well for other tangential reasons (no way to get url to current transaction, no way to patch params in the middle of a transaction), but the redirect part should facilitate it. This means that reconnecting from another client will not redirect again at this time. This behavior could be changed without a new SDK patch. --- src/classes/IntervalClient.ts | 14 +++++++ src/examples/basic/index.ts | 70 +++++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/internalRpcSchema.ts | 24 +++++++++++- src/ioSchema.ts | 24 +++++++----- src/types.ts | 4 ++ 6 files changed, 127 insertions(+), 10 deletions(-) diff --git a/src/classes/IntervalClient.ts b/src/classes/IntervalClient.ts index b66d1f6..5ba1bde 100644 --- a/src/classes/IntervalClient.ts +++ b/src/classes/IntervalClient.ts @@ -24,6 +24,7 @@ import { ActionResultSchema, IOFunctionReturnType, IO_RESPONSE, + LinkProps, T_IO_RESPONSE, } from '../ioSchema' import { IOClient } from './IOClient' @@ -652,6 +653,8 @@ export default class IntervalClient { }) }, }), + redirect: (props: LinkProps) => + this.#sendRedirect(transactionId, props), } const { io } = client @@ -904,4 +907,15 @@ export default class IntervalClient { timestamp: new Date().valueOf(), }) } + + async #sendRedirect(transactionId: string, props: LinkProps) { + const response = await this.#send('SEND_REDIRECT', { + transactionId, + ...props, + }) + + if (!response) { + throw new IntervalError('Failed sending redirect') + } + } } diff --git a/src/examples/basic/index.ts b/src/examples/basic/index.ts index 5aab4f2..582e4e6 100644 --- a/src/examples/basic/index.ts +++ b/src/examples/basic/index.ts @@ -120,6 +120,35 @@ const prod = new Interval({ }) return ctx.params }, + perform_redirect_flow: async () => { + let startedWork = false + const { workDone = false } = ctx.params + if (!workDone) { + await ctx.redirect({ + action: 'perform_common_work', + }) + startedWork = true + } + + console.log({ startedWork, workDone }) + + return { + startedWork, + workDone, + } + }, + perform_common_work: async () => { + ctx.loading.start( + 'Performing some work, will redirect back when complete' + ) + await sleep(2000) + await ctx.redirect({ + action: 'perform_redirect_flow', + params: { + workDone: true, + }, + }) + }, }, }) @@ -835,6 +864,47 @@ const interval = new Interval({ return { url: url.href } }, + redirect: async () => { + const [url, , action, paramsStr] = await io.group([ + io.input.url('Enter a URL').optional(), + io.display.markdown('--- or ---'), + io.input.text('Enter an action slug').optional(), + io.input + .text('With optional params', { + multiline: true, + }) + .optional(), + ]) + + let params = undefined + if (url) { + await ctx.redirect({ url: url.toString() }) + } else if (action) { + if (paramsStr) { + try { + params = JSON.parse(paramsStr) + } catch (err) { + ctx.log('Invalid params object', paramsStr) + } + } + + await ctx.redirect({ action, params }) + } else { + throw new Error('Must enter either a URL or an action slug') + } + + console.log({ + url, + action, + params, + }) + + return { + url: url?.toString(), + action, + paramsStr, + } + }, }, }) diff --git a/src/index.ts b/src/index.ts index db48543..bf9bfe2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -85,6 +85,7 @@ export const ctx: ActionCtx = { get organization() { return getActionStore().ctx.organization }, get action() { return getActionStore().ctx.action }, get notify() { return getActionStore().ctx.notify }, + get redirect() { return getActionStore().ctx.redirect }, } export default class Interval { diff --git a/src/internalRpcSchema.ts b/src/internalRpcSchema.ts index 0ec9de2..5cebbba 100644 --- a/src/internalRpcSchema.ts +++ b/src/internalRpcSchema.ts @@ -1,5 +1,9 @@ import { z } from 'zod' -import { deserializableRecord, serializableRecord } from './ioSchema' +import { + deserializableRecord, + linkSchema, + serializableRecord, +} from './ioSchema' export const DUPLEX_MESSAGE_SCHEMA = z.object({ id: z.string(), @@ -209,6 +213,15 @@ export const wsServerSchema = { }), returns: z.boolean(), }, + SEND_REDIRECT: { + inputs: z.intersection( + z.object({ + transactionId: z.string(), + }), + linkSchema + ), + returns: z.boolean(), + }, MARK_TRANSACTION_COMPLETE: { inputs: z.object({ transactionId: z.string(), @@ -330,6 +343,15 @@ export const clientSchema = { }), returns: z.boolean(), }, + REDIRECT: { + inputs: z.intersection( + z.object({ + transactionId: z.string(), + }), + linkSchema + ), + returns: z.boolean(), + }, } export type ClientSchema = typeof clientSchema diff --git a/src/ioSchema.ts b/src/ioSchema.ts index 49d44e1..916c28b 100644 --- a/src/ioSchema.ts +++ b/src/ioSchema.ts @@ -164,6 +164,18 @@ export const menuItem = z.intersection( ]) ) +export const linkSchema = z.union([ + z.object({ + url: z.string(), + }), + z.object({ + action: z.string(), + params: serializableRecord.optional(), + }), +]) + +export type LinkProps = z.infer + export const internalTableRow = z.object({ key: z.string(), data: tableRow, @@ -207,7 +219,7 @@ export const tableColumn = z.object({ }), z.object({ action: z.string(), - params: serializableRecord, + params: serializableRecord.optional(), }), z.object({}), ]) @@ -471,15 +483,9 @@ export const ioSchema = { }), z.union([ z.object({ - url: z.string().url(), - }), - z.object({ - href: z.string().url(), - }), - z.object({ - action: z.string(), - params: serializableRecord.optional(), + href: z.string(), }), + linkSchema, ]) ), state: z.null(), diff --git a/src/types.ts b/src/types.ts index 7a2ee29..da7c98f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,7 @@ import type { IOFunctionReturnType, T_IO_DISPLAY_METHOD_NAMES, T_IO_INPUT_METHOD_NAMES, + LinkProps, } from './ioSchema' import type { HostSchema } from './internalRpcSchema' import type { IOClient, IOClientRenderValidator } from './classes/IOClient' @@ -35,6 +36,7 @@ export type ActionCtx = Pick< loading: TransactionLoadingState log: ActionLogFn notify: NotifyFn + redirect: RedirectFn organization: { name: string slug: string @@ -184,6 +186,8 @@ export type NotifyConfig = { export type NotifyFn = (config: NotifyConfig) => Promise +export type RedirectFn = (props: LinkProps) => Promise + export type ResponseHandlerFn = (fn: T_IO_RESPONSE) => void export type Executor< From 3feba302350e50d0e3ffc7f5202369271bcf217e Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Thu, 25 Aug 2022 13:36:37 -0500 Subject: [PATCH 2/2] Set transaction resultStatus to REDIRECTED when redirect happens --- src/internalRpcSchema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internalRpcSchema.ts b/src/internalRpcSchema.ts index 5cebbba..b688c47 100644 --- a/src/internalRpcSchema.ts +++ b/src/internalRpcSchema.ts @@ -288,7 +288,7 @@ export const clientSchema = { TRANSACTION_COMPLETED: { inputs: z.object({ transactionId: z.string(), - resultStatus: z.enum(['SUCCESS', 'FAILURE', 'CANCELED']), + resultStatus: z.enum(['SUCCESS', 'FAILURE', 'CANCELED', 'REDIRECTED']), }), returns: z.void().nullable(), },