@habit.analytics/habit-smartlink-reactcomponent
Version:
A React component for Habit SmartLink integration.
263 lines (213 loc) • 9 kB
text/typescript
/**
* @file types.ts
* @description Shared type system for iframe ↔ parent postMessage communication.
*
* Direction is documented per message type as: child → parent | parent → child | both
*/
/*================================= SMARTLINK MESSAGE MAP ==================================*/
export type SmartlinkMessageMap = {
/** parent → child. Signals the smartlink to start. Sent in response to SMARTLINK_READY. */
SMARTLINK_INIT: undefined;
/** child → parent. Signals the child is mounted and ready to receive messages. */
SMARTLINK_READY: undefined;
/** child → parent. Signals parent to start the prepayment method flow. */
SMARTLINK_PREPAYMENT_METHOD: { quote_id: string };
/** parent → child. Signals the parent has completed the prepayment method flow. */
SMARTLINK_PREPAYMENT_METHOD_COMPLETE: {
success: boolean;
payment_id: string;
error?: string;
};
/** child → parent. Signals the current step completed successfully. */
SMARTLINK_STEP_COMPLETE: {
success?: boolean;
source?: "payments" | "uploader" | "consent" | string;
payment_data?: string;
consent_data?: string;
uploader_data?: string;
};
/** child → parent. Signals the user cancelled or closed the flow. */
SMARTLINK_CANCELLED: undefined;
/** child → parent. Reports an error to the parent. */
SMARTLINK_ERROR: { details?: unknown; message?: string };
/** child → parent. Requests the parent to resize the iframe element. */
SMARTLINK_RESIZE: { height: number; width?: number };
};
/*======================================= BASE TYPES =======================================*/
export type AnyMessageMap = SmartlinkMessageMap;
export type SmartlinkMessage<
K extends keyof SmartlinkMessageMap = keyof SmartlinkMessageMap,
> = {
type: K;
payload?: SmartlinkMessageMap[K];
requestId?: string;
};
export type AnyIframeMessage = SmartlinkMessage;
/*================================ REQUEST → RESPONSE MAP ==================================*/
/**
* Maps child → parent request types to the expected parent → child response type.
* Only message types with a known response are included — these are the only types
* accepted by the `request` function on SmartlinkChildAPI.
*/
export type SmartlinkRequestResponseMap = {
SMARTLINK_READY: SmartlinkMessage<"SMARTLINK_INIT">;
SMARTLINK_PREPAYMENT_METHOD: SmartlinkMessage<"SMARTLINK_PREPAYMENT_METHOD_COMPLETE">;
};
/*==================================== REQUEST ID HELPER ===================================*/
/**
* Generates a unique requestId for correlating request/response message pairs.
* Used internally by useSmartlinkMessaging — not typically called directly.
*/
export const createRequestId = (): string =>
`req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
/*===================================== FACTORY HELPER =====================================*/
export function createSmartlinkMessage<K extends keyof SmartlinkMessageMap>(
type: K,
payload?: SmartlinkMessageMap[K],
requestId?: string,
): SmartlinkMessage<K> {
return {
type,
...(payload !== undefined ? { payload } : {}),
...(requestId !== undefined ? { requestId } : {}),
};
}
/*================================== MESSAGE REQUEST TYPES =================================*/
/**
* Internal entry stored per in-flight request.
* Keyed by requestId in the pending map inside useSmartlinkMessaging.
*/
export interface PendingRequest<TResponse> {
resolve: (value: TResponse) => void;
reject: (reason: Error) => void;
timer: ReturnType<typeof setTimeout>;
}
/** Options passed to the `request` function. */
export interface MessageRequestOptions {
/**
* How long to wait for a response before rejecting.
* Defaults to 5000ms.
*/
timeout?: number;
}
/*======================================= SUBSCRIBER =======================================*/
/**
* isValid — true if the message passed origin check + selfcare revalidation.
* False means the parent would have silently dropped it internally; it is exposed
* here so the consumer can decide whether to act or log.
*/
export type SmartlinkSubscriber = (
message: SmartlinkMessage,
event: MessageEvent<SmartlinkMessage>,
isValid: boolean,
) => void;
/*================================= PARENT HOOK OPTIONS ====================================*/
interface BaseIframeMessagingOptions {
/** The full URL of the iframe src. Used to derive and validate the expected origin. */
pluginUrl: string;
/** Ref to the iframe DOM element. Used for postMessage and resize. */
iframeRef: React.RefObject<HTMLIFrameElement>;
/**
* Called when the child requests a resize.
* Receives the dimensions the child requested. Return overridden dimensions to clamp,
* or void to apply the requested values as-is.
*/
onResize?: (requested: {
height: number;
width?: number;
}) => { height: number; width?: number } | void;
}
export interface UseSelfcareParentMessagingOptions extends BaseIframeMessagingOptions {
/**
* Plugin ID used to look up and validate the plugin in the selfcare route registry.
* Required for revalidation — without it all messages will fail the security check.
*/
id: string;
}
export interface UseSmartlinkParentMessagingOptions extends BaseIframeMessagingOptions {}
/*================================ PARENT HOOK API — SMARTLINK =============================*/
export interface SmartlinkParentAPI {
/** Send any smartlink-namespaced message to the child. */
sendMessage: (message: SmartlinkMessage) => void;
/** Send SMARTLINK_INIT — signals the smartlink to start after SMARTLINK_READY. */
sendInit: (requestId?: string) => void;
/**
* Subscribe to all incoming smartlink messages from the child.
* isValid indicates whether the message passed the origin check.
* Returns unsubscribe fn.
*/
subscribe: (handler: SmartlinkSubscriber) => () => void;
/**
* Subscribe to a specific smartlink message type.
* isValid is passed as the third argument to the handler.
* Returns unsubscribe fn.
*/
subscribeTo: <K extends keyof SmartlinkMessageMap>(
type: K,
handler: (
message: SmartlinkMessage<K>,
event: MessageEvent<SmartlinkMessage>,
isValid: boolean,
) => void,
) => () => void;
}
/*================================ CHILD HOOK OPTIONS — SMARTLINK ==========================*/
export interface UseSmartlinkMessagingOptions {
/** Called on every incoming message before subscribers are notified. */
onMessage?: (message: SmartlinkMessage) => void;
/**
* Origin to target when sending postMessage to the parent.
* Defaults to '*'. Set to the known parent origin in production.
*/
targetOrigin?: string;
/**
* Origins from which to accept incoming messages.
* Defaults to '*'. Pass an array to restrict.
*/
acceptOrigins?: "*" | string[];
}
/*=============================== CHILD HOOK API — SMARTLINK ===============================*/
export interface SmartlinkChildAPI {
/** Send a raw smartlink message to the parent. */
sendMessage: (message: SmartlinkMessage) => void;
/** Send SMARTLINK_READY — call once on mount to trigger the init handshake. */
notifyReady: () => void;
/** Send SMARTLINK_STEP_COMPLETE — signal the current step finished. */
completeStep: (
result?: SmartlinkMessageMap["SMARTLINK_STEP_COMPLETE"] | undefined,
) => void;
/** Send SMARTLINK_CANCELLED — signal the user cancelled the flow. */
cancel: () => void;
/** Send SMARTLINK_RESIZE — ask the parent to resize the iframe. */
resize: (height: number, width?: number) => void;
/**
* Send a typed request and await the correlated response.
* Only message types present in SmartlinkRequestResponseMap are accepted.
* Rejects if no response arrives within the timeout (default 5000ms).
*/
request: <K extends keyof SmartlinkRequestResponseMap>(
type: K,
payload?: SmartlinkMessageMap[K],
options?: MessageRequestOptions,
) => Promise<SmartlinkRequestResponseMap[K]>;
/**
* Send SMARTLINK_PREPAYMENT_METHOD and await SMARTLINK_PREPAYMENT_METHOD_COMPLETE.
* Resolves with the full response message including success and payment_id.
*/
requestPrePaymentMethod: (
quoteId: string,
options?: MessageRequestOptions,
) => Promise<SmartlinkMessage<"SMARTLINK_PREPAYMENT_METHOD_COMPLETE">>;
/** Subscribe to all incoming messages from the parent. Returns unsubscribe fn. */
subscribe: (handler: SmartlinkSubscriber) => () => void;
/** Subscribe to a specific message type from the parent. Returns unsubscribe fn. */
subscribeTo: <K extends keyof SmartlinkMessageMap>(
type: K,
handler: (
message: SmartlinkMessage<K>,
event: MessageEvent<SmartlinkMessage>,
) => void,
) => () => void;
/** True once notifyReady() has been called. Reactive — triggers re-render. */
isReady: boolean;
}