diff --git a/farcaster-wp.php b/farcaster-wp.php
index 66b257e..8d108b8 100644
--- a/farcaster-wp.php
+++ b/farcaster-wp.php
@@ -10,7 +10,7 @@
* Plugin Name: Farcaster WP
* Plugin URI: https://farcaster-wp.davisshaver.com/
* Description: Farcaster WP connects your WordPress site to Farcaster.
- * Version: 0.0.16
+ * Version: 0.0.17
* Author: Davis Shaver
* Author URI: https://davisshaver.com/
* License: GPL v2 or later
@@ -22,7 +22,7 @@
defined( 'ABSPATH' ) || exit;
-define( 'FARCASTER_WP_VERSION', '0.0.16' );
+define( 'FARCASTER_WP_VERSION', '0.0.17' );
define( 'FARCASTER_WP_API_NAMESPACE', 'farcaster-wp/v1' );
define( 'FARCASTER_WP_API_URL', get_site_url() . '/wp-json/' . FARCASTER_WP_API_NAMESPACE );
diff --git a/includes/api/class-manifest-controller.php b/includes/api/class-manifest-controller.php
index 6770b49..f83a9a3 100644
--- a/includes/api/class-manifest-controller.php
+++ b/includes/api/class-manifest-controller.php
@@ -1,6 +1,6 @@
[
'header' => $header,
@@ -99,11 +101,14 @@ public function get_manifest() {
'iconUrl' => get_site_icon_url(),
'splashImageUrl' => $splash_image_url,
'splashBackgroundColor' => $splash_background_color,
- // @TODO: Add API endpoint for webhook to do... something?
- // 'webhookUrl' => '',
+
],
];
+ if ( Notifications::are_enabled() ) {
+ $manifest['frame']['webhookUrl'] = get_rest_url( null, FARCASTER_WP_API_NAMESPACE . '/webhook' );
+ }
+
return new WP_REST_Response( $manifest );
}
@@ -112,7 +117,7 @@ public function get_manifest() {
*
* @return array
*/
- public function get_app_config_schema() {
+ public function get_manifest_schema() {
return [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->resource_name,
diff --git a/includes/api/class-webhook-controller.php b/includes/api/class-webhook-controller.php
new file mode 100644
index 0000000..628676d
--- /dev/null
+++ b/includes/api/class-webhook-controller.php
@@ -0,0 +1,133 @@
+namespace,
+ '/' . $this->resource_name,
+ [
+ [
+ 'methods' => 'POST',
+ 'callback' => [ $this, 'process_webhook' ],
+ 'validate_callback' => [ $this, 'validate_webhook' ],
+ ],
+ 'schema' => [ $this, 'get_webhook_schema' ],
+ ]
+ );
+ }
+
+ /**
+ * Process the webhook.
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function process_webhook( $request ) {
+ $body = $request->get_body();
+ $data = json_decode( $body, true );
+ $header = json_decode( base64_decode( $data['header'] ), true );
+ $payload = json_decode( base64_decode( $data['payload'] ), true );
+ $response = Notifications::process_webhook( $header, $payload );
+ return new WP_REST_Response( $response );
+ }
+
+ /**
+ * Validate the webhook.
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return bool True if the webhook is valid, false otherwise.
+ */
+ public function validate_webhook( $request ) {
+ $body = $request->get_body();
+ $data = json_decode( $body, true );
+
+ if ( empty( $data['header'] ) || empty( $data['payload'] ) || empty( $data['signature'] ) ) {
+ return new WP_Error( 'invalid_webhook_parameters', 'Invalid webhook parameters', [ 'status' => 400 ] );
+ }
+
+ $header = json_decode( base64_decode( $data['header'] ), true );
+ $payload = json_decode( base64_decode( $data['payload'] ), true );
+
+ if ( empty( $header['fid'] ) || empty( $header['type'] ) || empty( $header['key'] ) ) {
+ return new WP_Error( 'invalid_webhook_header', 'Invalid webhook header', [ 'status' => 400 ] );
+ }
+
+ if ( empty( $payload['event'] ) || ! in_array( $payload['event'], [ 'frame_added', 'frame_removed', 'notifications_disabled', 'notifications_enabled' ] ) ) {
+ return new WP_Error( 'invalid_webhook_payload', 'Invalid webhook payload', [ 'status' => 400 ] );
+ }
+
+ // We are only handling frame_added and notifications_enabled events if they have notificationDetails.
+ if (
+ in_array( $payload['event'], [ 'frame_added', 'notifications_enabled' ] ) &&
+ ( empty( $payload['notificationDetails'] ) || empty( $payload['notificationDetails']['url'] ) || empty( $payload['notificationDetails']['token'] ) )
+ ) {
+ return new WP_Error( 'invalid_notification_details', 'Invalid notification details', [ 'status' => 400 ] );
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the REST schema for the endpoints.
+ *
+ * @return array
+ */
+ public function get_webhook_schema() {
+ return [
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->resource_name,
+ 'type' => 'object',
+ 'properties' => [
+ 'header' => [
+ 'required' => true,
+ 'type' => 'string',
+ ],
+ 'payload' => [
+ 'required' => true,
+ 'type' => 'string',
+ ],
+ 'signature' => [
+ 'required' => true,
+ 'type' => 'string',
+ ],
+ ],
+ ];
+ }
+}
diff --git a/includes/class-admin.php b/includes/class-admin.php
index 3fbec09..b47c479 100644
--- a/includes/class-admin.php
+++ b/includes/class-admin.php
@@ -114,6 +114,7 @@ public static function action_init() {
'url' => '',
),
'domain_manifest' => '',
+ 'notifications_enabled' => false,
);
$schema = array(
'type' => 'object',
@@ -121,6 +122,9 @@ public static function action_init() {
'frames_enabled' => array(
'type' => 'boolean',
),
+ 'notifications_enabled' => array(
+ 'type' => 'boolean',
+ ),
'splash_background_color' => array(
'type' => 'string',
),
diff --git a/includes/class-api.php b/includes/class-api.php
index 5c7da50..1d5761d 100644
--- a/includes/class-api.php
+++ b/includes/class-api.php
@@ -8,6 +8,7 @@
namespace Farcaster_WP;
use Farcaster_WP\API\Manifest_Controller;
+use Farcaster_WP\API\Webhook_Controller;
defined( 'ABSPATH' ) || exit;
@@ -23,5 +24,11 @@ public static function init() {
include_once 'api/class-manifest-controller.php';
$manifest_api = new Manifest_Controller();
add_action( 'rest_api_init', [ $manifest_api, 'register_routes' ] );
+
+ if ( Notifications::are_enabled() ) {
+ include_once 'api/class-webhook-controller.php';
+ $webhook_api = new Webhook_Controller();
+ add_action( 'rest_api_init', [ $webhook_api, 'register_routes' ] );
+ }
}
}
diff --git a/includes/class-frames.php b/includes/class-frames.php
index e7c08e9..2c58a1e 100644
--- a/includes/class-frames.php
+++ b/includes/class-frames.php
@@ -130,8 +130,9 @@ public static function action_wp_head() {
* Enqueue scripts.
*/
public static function action_enqueue_scripts() {
- $options = get_option( 'farcaster_wp', array() );
-
+ $options = get_option( 'farcaster_wp', array() );
+ $notifications_enabled = $options['notifications_enabled'] ?? false;
+
// Only enqueue if frames are enabled in settings.
if ( ! empty( $options['frames_enabled'] ) ) {
wp_enqueue_script(
@@ -144,6 +145,13 @@ public static function action_enqueue_scripts() {
'strategy' => 'defer',
)
);
+ wp_localize_script(
+ 'farcaster-frame-sdk',
+ 'farcasterWP',
+ array(
+ 'notificationsEnabled' => $notifications_enabled,
+ )
+ );
}
}
}
diff --git a/includes/class-initializer.php b/includes/class-initializer.php
index 514ad9e..8a5cd93 100644
--- a/includes/class-initializer.php
+++ b/includes/class-initializer.php
@@ -19,5 +19,6 @@ public static function init() {
Admin::init();
API::init();
Frames::init();
+ Notifications::init();
}
}
diff --git a/includes/class-notifications.php b/includes/class-notifications.php
new file mode 100644
index 0000000..0587e34
--- /dev/null
+++ b/includes/class-notifications.php
@@ -0,0 +1,378 @@
+post_type ) {
+ self::schedule_publish_post_notifications( $post->ID );
+ }
+ }
+
+ /**
+ * Schedule the notifications.
+ *
+ * @param int $post_id The post ID.
+ */
+ public static function schedule_publish_post_notifications( $post_id ) {
+ wp_schedule_single_event( time(), 'farcaster_wp_send_publish_post_notifications', array( $post_id ) );
+ }
+
+ /**
+ * Send the notifications.
+ *
+ * @param int $post_id The post ID.
+ */
+ public static function send_publish_post_notifications( $post_id ) {
+ $post = get_post( $post_id );
+ if ( ! empty( $post ) ) {
+ self::initiate_notifications( $post );
+ }
+ }
+
+ /**
+ * Get the notification ID.
+ *
+ * @param int $post_id The post ID.
+ * @return string The notification ID.
+ */
+ public static function get_notification_id( $post_id ) {
+ $blog_name = str_replace( '-', '_', sanitize_title_with_dashes( get_bloginfo( 'name' ) ) );
+ return 'farcaster_wp_notification_' . $post_id . '_' . $blog_name;
+ }
+
+ /**
+ * Get the tokens by url.
+ *
+ * @return array The tokens by url.
+ */
+ public static function get_tokens_by_url() {
+ $subscriptions = get_option( self::$notifications_option_name, array() );
+ $tokens_by_url = array();
+ foreach ( $subscriptions as $fid => $apps ) {
+ foreach ( $apps as $app_key => $app ) {
+ $tokens_by_url[ $app['url'] ][] = $app['token'];
+ }
+ }
+ return $tokens_by_url;
+ }
+
+ /**
+ * Retry notifications.
+ *
+ * @param string $url The URL.
+ * @param array $tokens The tokens.
+ * @param int $post_id The post ID.
+ */
+ public static function retry_notifications( $url, $tokens, $post_id ) {
+ $notification_body = self::get_notification_body( $post_id );
+ self::chunk_and_send_notifications( $tokens, $notification_body, $url, $post_id );
+ }
+
+ /**
+ * Send the notifications.
+ *
+ * @param object $post The post object.
+ */
+ public static function initiate_notifications( $post ) {
+ $tokens_by_app = self::get_tokens_by_url();
+ self::send_notifications_by_app( $tokens_by_app, $post->ID );
+ }
+
+ /**
+ * Get the notification body.
+ *
+ * @param int $post_id The post ID.
+ * @return array The notification body.
+ */
+ public static function get_notification_body( $post_id ) {
+ return array(
+ 'notificationId' => self::get_notification_id( $post_id ),
+ 'title' => substr( wp_strip_all_tags( get_the_title( $post_id ) ), 0, 32 ),
+ 'body' => substr( wp_strip_all_tags( get_the_excerpt( $post_id ) ), 0, 128 ),
+ 'targetUrl' => get_permalink( $post_id ),
+ );
+ }
+
+ /**
+ * Send the notifications by app.
+ *
+ * @param array $tokens_by_app The tokens by app.
+ * @param int $post_id The post ID.
+ */
+ public static function send_notifications_by_app( $tokens_by_app, $post_id ) {
+ $notification_body = self::get_notification_body( $post_id );
+ foreach ( $tokens_by_app as $url => $tokens ) {
+ self::chunk_and_send_notifications( $tokens, $notification_body, $url, $post_id );
+ }
+ }
+
+ /**
+ * Send the notification.
+ *
+ * @param string $url The URL.
+ * @param array $notification_body The notification body.
+ * @param int $post_id The post ID.
+ * @return array The response.
+ */
+ public static function send_notification( $url, $notification_body, $post_id ) {
+ $response = wp_safe_remote_post( $url, array( 'body' => $notification_body ) );
+ if ( is_wp_error( $response ) ) {
+ $admin_email = get_option( 'admin_email' );
+ $error_message = $response->get_error_message();
+
+ // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
+ wp_mail(
+ $admin_email,
+ 'Farcaster Notification Error',
+ sprintf(
+ 'There was an error sending notifications: %s. A retry has been scheduled.',
+ $error_message
+ )
+ );
+
+ $unsent_tokens = $notification_body['tokens'];
+ wp_schedule_single_event(
+ time() + 300,
+ 'farcaster_wp_retry_notifications',
+ array(
+ $url,
+ $unsent_tokens,
+ $post_id,
+ )
+ );
+ return array(); // Return empty array since request failed.
+ }
+ return json_decode( wp_remote_retrieve_body( $response ), true );
+ }
+
+ /**
+ * Chunk and send the notifications.
+ *
+ * @param array $tokens The tokens.
+ * @param array $notification_body The notification body.
+ * @param string $url The URL.
+ * @param int $post_id The post ID.
+ */
+ public static function chunk_and_send_notifications( $tokens, $notification_body, $url, $post_id ) {
+ $chunks = array_chunk( $tokens, 100 );
+ foreach ( $chunks as $chunk ) {
+ $notification_body['tokens'] = $chunk;
+ $webhook_response = self::send_notification( $url, $notification_body, $post_id );
+
+ if ( ! empty( $webhook_response['result'] ) ) {
+ // Add succesful tokens to post meta.
+ if ( ! empty( $webhook_response['result']['successfulTokens'] ) ) {
+ $current_tokens = get_post_meta( $post_id, 'farcaster_wp_tokens', true );
+ $current_tokens = is_array( $current_tokens ) ? $current_tokens : array();
+ $current_tokens = array_merge( $current_tokens, $webhook_response['result']['successfulTokens'] );
+ update_post_meta( $post_id, 'farcaster_wp_tokens', $current_tokens );
+ }
+
+ // Process invalid tokens and remove them from the subscription.
+ if ( ! empty( $webhook_response['result']['invalidTokens'] ) ) {
+ foreach ( $webhook_response['result']['invalidTokens'] as $invalid_token ) {
+ self::remove_subscription_by_token( $invalid_token, $url );
+ }
+ }
+
+ // Schedule a retry for the rate limited tokens.
+ if ( ! empty( $webhook_response['result']['rateLimitedTokens'] ) ) {
+ $rate_limited_tokens = $webhook_response['result']['rateLimitedTokens'];
+ wp_schedule_single_event(
+ time() + 300,
+ 'farcaster_wp_retry_notifications',
+ array(
+ $url,
+ $rate_limited_tokens,
+ $post_id,
+ )
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Process the webhook.
+ *
+ * @param array $header The webhook header.
+ * @param array $payload The webhook payload.
+ * @return array The response.
+ * @throws \Exception If the event is invalid.
+ */
+ public static function process_webhook( $header, $payload ) {
+ $event = $payload['event'];
+ switch ( $event ) {
+ case 'frame_added':
+ return self::process_frame_added( $header, $payload );
+ case 'frame_removed':
+ return self::process_frame_removed( $header );
+ case 'notifications_disabled':
+ return self::process_notifications_disabled( $header );
+ case 'notifications_enabled':
+ return self::process_notifications_enabled( $header, $payload );
+ }
+ throw new \Exception( 'Invalid event: ' . esc_html( $event ) );
+ }
+
+ /**
+ * Add a subscription.
+ *
+ * Note: This data structure means that each FID + app key combo can only have one subscription.
+ *
+ * @param int $fid The Farcaster ID.
+ * @param string $key The app key (onchainer signer) public key.
+ * @param string $url The URL for notifications.
+ * @param string $token The token for notifications.
+ * @return array The response.
+ */
+ public static function add_subscription( $fid, $key, $url, $token ) {
+ $current_subscriptions = get_option( self::$notifications_option_name, array() );
+ $current_subscriptions[ $fid ][ $key ] = [
+ 'url' => $url,
+ 'token' => $token,
+ ];
+ update_option( self::$notifications_option_name, $current_subscriptions );
+ return [ 'success' => true ];
+ }
+
+ /**
+ * Remove a subscription by token.
+ *
+ * @param string $token The token.
+ * @param string $url The URL.
+ */
+ public static function remove_subscription_by_token( $token, $url ) {
+ $current_subscriptions = get_option( self::$notifications_option_name, array() );
+ foreach ( $current_subscriptions as $fid => $apps ) {
+ foreach ( $apps as $app_key => $app ) {
+ if ( $app['url'] === $url && $app['token'] === $token ) {
+ self::remove_subscription( $fid, $app_key );
+ }
+ }
+ }
+ }
+
+ /**
+ * Remove a subscription.
+ *
+ * @param int $fid The Farcaster ID.
+ * @param string $key The app key (onchainer signer) public key.
+ * @return array The response.
+ */
+ private static function remove_subscription( $fid, $key ) {
+ $current_subscriptions = get_option( self::$notifications_option_name, array() );
+ if ( ! empty( $current_subscriptions[ $fid ] ) && ! empty( $current_subscriptions[ $fid ][ $key ] ) ) {
+ unset( $current_subscriptions[ $fid ][ $key ] );
+ update_option( self::$notifications_option_name, $current_subscriptions );
+ }
+ return [ 'success' => true ];
+ }
+
+ /**
+ * Process the frame added event.
+ *
+ * @param array $header The webhook header.
+ * @param array $payload The webhook payload.
+ * @return array The response.
+ */
+ public static function process_frame_added( $header, $payload ) {
+ $fid = $header['fid'];
+ $key = $header['key'];
+ $url = $payload['notificationDetails']['url'];
+ $token = $payload['notificationDetails']['token'];
+ return self::add_subscription( $fid, $key, $url, $token );
+ }
+
+ /**
+ * Process the frame removed event.
+ *
+ * @param array $header The webhook header.
+ * @return array The response.
+ */
+ public static function process_frame_removed( $header ) {
+ $fid = $header['fid'];
+ $key = $header['key'];
+ return self::remove_subscription( $fid, $key );
+ }
+
+ /**
+ * Process the notifications disabled event.
+ *
+ * @param array $header The webhook header.
+ * @return array The response.
+ */
+ public static function process_notifications_disabled( $header ) {
+ $fid = $header['fid'];
+ $key = $header['key'];
+ $current_subscriptions = get_option( self::$notifications_option_name, array() );
+ if ( ! empty( $current_subscriptions[ $fid ] ) && ! empty( $current_subscriptions[ $fid ][ $key ] ) ) {
+ unset( $current_subscriptions[ $fid ][ $key ] );
+ update_option( self::$notifications_option_name, $current_subscriptions );
+ }
+ return [ 'success' => true ];
+ }
+
+ /**
+ * Process the notifications enabled event.
+ *
+ * @param array $header The webhook header.
+ * @param array $payload The webhook payload.
+ * @return array The response.
+ */
+ public static function process_notifications_enabled( $header, $payload ) {
+ $fid = $header['fid'];
+ $key = $header['key'];
+ $url = $payload['notificationDetails']['url'];
+ $token = $payload['notificationDetails']['token'];
+ return self::add_subscription( $fid, $key, $url, $token );
+ }
+}
diff --git a/package.json b/package.json
index afc7fa7..76d61cd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "farcaster-wp",
- "version": "0.0.16",
+ "version": "0.0.17",
"description": "Farcaster WP connects your WordPress site to Farcaster.",
"author": "Davis Shaver",
"license": "GPL-2.0-or-later",
@@ -49,6 +49,6 @@
"lint:php": "./vendor/bin/phpcs",
"lint:ts": "tsc --noEmit",
"lint:pkg-json": "wp-scripts lint-pkg-json",
- "start": "wp-scripts start"
+ "start": "wp-scripts start src/index src/sdk"
}
}
diff --git a/readme.txt b/readme.txt
index 30af237..9e5c134 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: WordPress, web3, Farcaster, Ethereum
Tested up to: 6.7.1
Requires at least: 6.7.0
Requires PHP: 7.0
-Stable tag: 0.0.16
+Stable tag: 0.0.17
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -26,7 +26,7 @@ Farcaster WP makes it easy to setup [Farcaster frames](https://docs.farcaster.xy
== Changelog ==
-= 0.0.16 =
+= 0.0.17 =
* Initial plugin release to WordPress.org
== Screenshots =
diff --git a/src/components/Controls.tsx b/src/components/Controls.tsx
index 50530f9..e6be504 100644
--- a/src/components/Controls.tsx
+++ b/src/components/Controls.tsx
@@ -61,6 +61,17 @@ const UseTitleAsButtonTextControl = ( { value, onChange } ) => {
);
};
+const NotificationsEnabledControl = ( { value, onChange } ) => {
+ return (
+
+ );
+};
+
const FramesEnabledControl = ( { value, onChange } ) => {
return (
) }
+ { mismatches.details.webhookUrl && (
+
+ { __(
+ 'The manifest webhook URL does not match the current site webhook URL.',
+ 'farcaster-wp'
+ ) }
+
+ ) }
{ mismatches.details.header && (
{
setUseTitleAsButtonText,
domainManifest,
setDomainManifest,
+ notificationsEnabled,
+ setNotificationsEnabled,
} = useSettings();
const { manifest, fetchManifest } = useManifest();
@@ -132,6 +135,16 @@ const SettingsPage = () => {
+
+
+
+
+
+
+
{
const [ domainManifest, setDomainManifest ] = useState< string >();
const [ framesEnabled, setFramesEnabled ] = useState< boolean >();
+ const [ notificationsEnabled, setNotificationsEnabled ] =
+ useState< boolean >( false );
const [ splashBackgroundColor, setSplashBackgroundColor ] =
useState< string >();
const [ buttonText, setButtonText ] = useState< string >();
@@ -65,6 +68,9 @@ export const useSettings = () => {
settings.farcaster_wp.use_title_as_button_text
);
setDomainManifest( settings.farcaster_wp.domain_manifest );
+ setNotificationsEnabled(
+ settings.farcaster_wp.notifications_enabled
+ );
}
);
}, [] );
@@ -107,6 +113,7 @@ export const useSettings = () => {
fallback_image: fallbackImage,
use_title_as_button_text: useTitleAsButtonText,
domain_manifest: domainManifest,
+ notifications_enabled: notificationsEnabled,
},
},
} )
@@ -155,5 +162,7 @@ export const useSettings = () => {
setUseTitleAsButtonText,
domainManifest,
setDomainManifest,
+ notificationsEnabled,
+ setNotificationsEnabled,
};
};
diff --git a/src/sdk.ts b/src/sdk.ts
index 033687e..d3116b7 100644
--- a/src/sdk.ts
+++ b/src/sdk.ts
@@ -1,3 +1,94 @@
-import { sdk } from '@farcaster/frame-sdk';
+import sdk from '@farcaster/frame-sdk';
+import { showToast } from './utils/toast';
-sdk.actions.ready();
+declare global {
+ interface Window {
+ farcasterWP: {
+ notificationsEnabled: boolean;
+ };
+ }
+}
+
+const loadSdk = async () => {
+ const context = await sdk.context;
+ sdk.actions.ready();
+
+ if ( ! window.farcasterWP.notificationsEnabled ) {
+ return;
+ }
+
+ if ( ! context ) {
+ // No context, probably not in frame.
+ return;
+ }
+
+ if ( context?.location?.type === 'notification' ) {
+ showToast( {
+ type: 'success',
+ message: 'Thanks for being a subscriber!',
+ duration: 3000,
+ } );
+ return;
+ }
+
+ sdk.actions
+ .addFrame()
+ .then( ( result ) => {
+ if ( result?.added ) {
+ showToast( {
+ type: 'success',
+ message: 'You are subscribed to notifications.',
+ duration: 3000,
+ } );
+ } else {
+ showToast( {
+ message:
+ 'Would you like to get Warpcast notifications about new posts?',
+ buttonText: 'Yes!',
+ onButtonClick: () => {
+ sdk.actions
+ .addFrame()
+ .then( ( retryResult ) => {
+ // @TODO Technically we could subscribe the user here, but
+ // for now we are going to wait for the webhook event to arrive.
+ if ( retryResult && retryResult?.added ) {
+ showToast( {
+ message:
+ 'Successfully subscribed to notifications!',
+ duration: 3000,
+ } );
+ } else {
+ showToast( {
+ type: 'error',
+ message:
+ 'Error adding frame, addFrame response: ' +
+ JSON.stringify( retryResult ),
+ duration: 3000,
+ } );
+ }
+ } )
+ .catch( ( error ) => {
+ showToast( {
+ type: 'error',
+ message:
+ 'Error adding frame, addFrame error: ' +
+ JSON.stringify( error ),
+ duration: 3000,
+ } );
+ } );
+ },
+ } );
+ }
+ } )
+ .catch( ( error ) => {
+ showToast( {
+ type: 'error',
+ message:
+ 'Error adding frame, addFrame error: ' +
+ JSON.stringify( error ),
+ duration: 3000,
+ } );
+ } );
+};
+
+loadSdk();
diff --git a/src/utils/toast.ts b/src/utils/toast.ts
new file mode 100644
index 0000000..336288b
--- /dev/null
+++ b/src/utils/toast.ts
@@ -0,0 +1,69 @@
+interface ToastOptions {
+ buttonText?: string;
+ duration?: number;
+ message: string;
+ onButtonClick?: () => void;
+ type?: 'prompt' | 'success' | 'error';
+}
+
+export const showToast = ( {
+ buttonText,
+ duration = 10000,
+ message,
+ onButtonClick,
+ type = 'prompt',
+}: ToastOptions ) => {
+ const toast = document.createElement( 'div' );
+
+ const backgroundColorMap = {
+ error: '#FF453A',
+ prompt: '#472A91',
+ success: '#4CAF50',
+ };
+
+ toast.style.cssText = `
+ align-items: center;
+ background: ${ backgroundColorMap[ type ] };
+ border-radius: 4px;
+ bottom: 20px;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
+ color: white;
+ display: flex;
+ gap: 12px;
+ left: 50%;
+ max-width: calc(100% - 80px);
+ padding: 12px 24px;
+ position: fixed;
+ transform: translateX(-50%);
+ width: max-content;
+ z-index: 10000;
+ `;
+
+ toast.textContent = message;
+
+ if ( buttonText && onButtonClick ) {
+ const button = document.createElement( 'button' );
+ button.textContent = buttonText;
+ button.style.cssText = `
+ background: #7C65C1;
+ border-radius: 4px;
+ border: none;
+ color: white;
+ cursor: pointer;
+ padding: 8px 16px;
+ `;
+ button.onclick = () => {
+ onButtonClick();
+ document.body.removeChild( toast );
+ };
+ toast.appendChild( button );
+ }
+
+ document.body.appendChild( toast );
+
+ setTimeout( () => {
+ if ( document.body.contains( toast ) ) {
+ document.body.removeChild( toast );
+ }
+ }, duration );
+};