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..b688c47 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(), @@ -275,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(), }, @@ -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 46ef530..f4150f8 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({}), ]) @@ -472,15 +484,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<