UNPKG

@metamask/snaps-rpc-methods

Version:
229 lines 8.21 kB
import { PermissionType, SubjectType } from "@metamask/permission-controller"; import { rpcErrors } from "@metamask/rpc-errors"; import { enumValue, NotificationType, union, ContentType, getErrorMessage, ComponentOrElementStruct } from "@metamask/snaps-sdk"; import { createUnion, validateLink, validateTextLinks } from "@metamask/snaps-utils"; import { object, string, optional } from "@metamask/superstruct"; import { assertExhaustive, hasProperty, isObject } from "@metamask/utils"; const methodName = 'snap_notify'; const NativeNotificationStruct = object({ type: enumValue(NotificationType.Native), message: string(), }); const InAppNotificationStruct = object({ type: enumValue(NotificationType.InApp), message: string(), }); const InAppNotificationWithDetailsStruct = object({ type: enumValue(NotificationType.InApp), message: string(), content: ComponentOrElementStruct, title: string(), footerLink: optional(object({ href: string(), text: string(), })), }); const NotificationParametersStruct = union([ InAppNotificationStruct, InAppNotificationWithDetailsStruct, NativeNotificationStruct, ]); /** * The specification builder for the `snap_notify` permission. * `snap_notify` allows snaps to send multiple types of notifications to its users. * * @param options - The specification builder options. * @param options.allowedCaveats - The optional allowed caveats for the permission. * @param options.messenger - The messenger. * @param options.methodHooks - The RPC method hooks needed by the method implementation. * @returns The specification for the `snap_notify` permission. */ export const specificationBuilder = ({ allowedCaveats = null, methodHooks, messenger, }) => { return { permissionType: PermissionType.RestrictedMethod, targetName: methodName, allowedCaveats, methodImplementation: getImplementation({ methodHooks, messenger }), subjectTypes: [SubjectType.Snap], }; }; const methodHooks = { isOnPhishingList: true, maybeUpdatePhishingList: true, }; /** * Display a * [notification](https://docs.metamask.io/snaps/features/notifications/) in * MetaMask or natively in the OS. Snaps can trigger a short (up to 80 * characters) notification message for actionable or time sensitive * information. `inApp` notifications can also include an optional * [expanded view](https://docs.metamask.io/snaps/features/notifications/#expanded-view). * The expanded view has a title, content, and optional footer link shown when * a user clicks on the notification. * * @example Basic in app notification * ```json name="Manifest" * { * "initialPermissions": { * "snap_notify": {} * } * } * ``` * ```ts name="Usage" * snap.request({ * method: 'snap_notify', * params: { * type: 'inApp', * message: 'This is an in-app notification', * }, * }); * ``` * * @example Expandable in app notification * ```json name="Manifest" * { * "initialPermissions": { * "snap_notify": {} * } * } * ``` * ```ts name="Usage" * snap.request({ * method: 'snap_notify', * params: { * type: 'inApp', * message: 'This is an in-app notification', * title: 'Notification Title', * content: ( * <Box> * <Text>This is the expanded content of the notification.</Text> * </Box> * ), * footerLink: { * href: 'https://example.com', * text: 'Click here for more info', * }, * }, * }); * ``` * * @example Native notification * ```json name="Manifest" * { * "initialPermissions": { * "snap_notify": {} * } * } * ``` * ```ts name="Usage" * snap.request({ * method: 'snap_notify', * params: { * type: 'native', * message: 'This is a native notification', * }, * }); * ``` */ export const notifyBuilder = Object.freeze({ targetName: methodName, specificationBuilder, methodHooks, actionNames: [ 'RateLimitController:call', 'SnapController:getSnap', 'SnapInterfaceController:createInterface', ], }); /** * Builds the method implementation for `snap_notify`. * * @param options - The options. * @param options.messenger - The messenger. * @param options.methodHooks - The RPC method hooks. * @param options.methodHooks.isOnPhishingList - A function that checks for links against the phishing list. * @param options.methodHooks.maybeUpdatePhishingList - A function that updates the phishing list if needed. * @returns The method implementation which returns `null` on success. * @throws If the params are invalid. */ export function getImplementation({ methodHooks: { isOnPhishingList, maybeUpdatePhishingList }, messenger, }) { return async function implementation(args) { const { params, context: { origin }, } = args; await maybeUpdatePhishingList(); const validatedParams = getValidatedParams(params, isOnPhishingList, messenger); if (hasProperty(validatedParams, 'content')) { const id = messenger.call('SnapInterfaceController:createInterface', origin, validatedParams.content, undefined, ContentType.Notification); validatedParams.content = id; } switch (validatedParams.type) { case NotificationType.Native: return (await messenger.call('RateLimitController:call', origin, 'showNativeNotification', origin, validatedParams.message)); case NotificationType.InApp: { const { content, message, title, footerLink } = validatedParams; return (await messenger.call('RateLimitController:call', origin, 'showInAppNotification', origin, { interfaceId: content, message, title, footerLink, })); } /* istanbul ignore next */ default: return assertExhaustive(validatedParams.type); } }; } /** * Validates the notify method `params` and returns them cast to the correct * type. Throws if validation fails. * * @param params - The unvalidated params object from the method request. * @param isOnPhishingList - The function that checks for links against the phishing list. * @param messenger - The messenger. * @returns The validated method parameter object. * @throws If the params are invalid. */ export function getValidatedParams(params, isOnPhishingList, messenger) { if (!isObject(params)) { throw rpcErrors.invalidParams({ message: 'Expected params to be a single object.', }); } const { type, message } = params; if (!type || typeof type !== 'string' || !Object.values(NotificationType).includes(type)) { throw rpcErrors.invalidParams({ message: 'Must specify a valid notification "type".', }); } const isNotString = !message || typeof message !== 'string'; // Set to the max message length on a Mac notification for now. if (type === NotificationType.Native && (isNotString || message.length >= 50)) { throw rpcErrors.invalidParams({ message: 'Must specify a non-empty string "message" less than 50 characters long.', }); } if (type === NotificationType.InApp && (isNotString || message.length >= 500)) { throw rpcErrors.invalidParams({ message: 'Must specify a non-empty string "message" less than 500 characters long.', }); } try { const validatedParams = createUnion(params, NotificationParametersStruct, 'type'); const getSnap = (snapId) => messenger.call('SnapController:getSnap', snapId); validateTextLinks(validatedParams.message, isOnPhishingList, getSnap); if (hasProperty(validatedParams, 'footerLink')) { validateLink(validatedParams.footerLink.href, isOnPhishingList, getSnap); } return validatedParams; } catch (error) { throw rpcErrors.invalidParams({ message: `Invalid params: ${getErrorMessage(error)}`, }); } } //# sourceMappingURL=notify.mjs.map