Skip to content

[FEATURE] OneSignal Helper #29

@NurhayatYurtaslan

Description

@NurhayatYurtaslan

Prerequisites

  • I have searched for existing feature requests that might be similar
  • I have read the documentation
  • This feature would benefit the broader MasterFabric community

Target Component

Core App

Feature Type

New screen/page

Screen/Feature (optional)

None

Priority

High - Important for my use case

Problem Statement

Currently, when developers need to integrate OneSignal push notifications in MasterFabric Expo apps, they face several challenges:

  • Complex Setup: OneSignal requires complex initialization with multiple configuration options
  • Code Duplication: Developers repeatedly write boilerplate code for permission requests, subscription management, and event handling
  • Inconsistency: Different modules may implement OneSignal differently, leading to inconsistent behavior
  • Permission Management: No unified permission handling - should use permissionsHandler helper for notification permissions instead of OneSignal's native methods
  • Event Handling: No unified API for handling notification events (received, opened, dismissed)
  • User Management: Manual implementation of user ID management, external user IDs, and user data
  • Tag Management: No centralized API for managing user tags and segments
  • Testing: Difficult to test push notification functionality without a unified interface
  • Platform Differences: Handling iOS vs Android differences requires platform-specific code, especially for permissions

This leads to:

  • Repetitive boilerplate code across projects
  • Inconsistent user experience with push notifications
  • Bugs from improper initialization or event handling
  • Poor developer experience when all that's needed is to set up push notifications
  • Difficult maintenance and testing
  • Platform-specific bugs that are hard to catch
  • Missing notification analytics and tracking

Proposed Solution

Create a comprehensive onesignal_helper.ts utility module in packages/masterfabric-expo-core/src/helpers/ that provides:

  1. Initialization Functions

    • Initialize OneSignal with app ID
    • Configure notification handlers
    • Set up event listeners
  2. Permission Management

    • Uses permissionsHandler helper for notification permission requests
    • Integrates with permissionsHandler.requestNotifications() for unified permission handling
    • Check permission status via permissions handler
    • Handle permission denied scenarios with permissions handler utilities
  3. User Subscription Management

    • Get subscription status
    • Enable/disable push notifications
    • Get push token/player ID
    • Handle subscription state changes
  4. User ID Management

    • Set/get user ID
    • Set/get external user ID
    • Set user email
    • Set user phone number
    • Set user data (custom attributes)
  5. Tag Management

    • Add/remove tags
    • Get all tags
    • Set tags in bulk
    • Clear all tags
  6. Notification Event Handling

    • Handle notification received
    • Handle notification opened
    • Handle notification dismissed
    • Handle in-app message displayed
    • Handle in-app message action
  7. Notification Display

    • Show notification programmatically
    • Customize notification appearance
    • Handle notification actions
  8. Analytics & Tracking

    • Track notification events
    • Get notification statistics
    • Track user engagement

Alternatives Considered

  1. Using OneSignal SDK directly without helpers

    • Too much boilerplate code
    • Inconsistent implementations
    • Poor developer experience
    • Manual event handling setup
  2. Using other push notification services

    • Firebase Cloud Messaging (FCM)
    • Expo Notifications
    • AWS SNS
    • But OneSignal provides better features (tags, segments, analytics)
  3. Creating project-specific helpers

    • Code duplication
    • Inconsistent implementations
    • Maintenance burden
    • Security vulnerabilities

Use Cases

  1. When initializing push notifications on app start

    • Need to initialize OneSignal with app ID
    • Need to set up event handlers
    • Example: onesignalHelper.initialize('your-app-id', { onNotificationReceived: handleNotification })
  2. When requesting notification permissions

    • Need to request permissions from user using permissionsHandler
    • Need to handle permission denied with permissions handler utilities
    • Example: const status = await permissionsHandler.requestNotifications({ alert: true, badge: true, sound: true }) then await onesignalHelper.setSubscription(status.granted)
  3. When managing user subscription

    • Need to enable/disable push notifications
    • Need to check subscription status
    • Example: await onesignalHelper.setSubscription(true) and const isSubscribed = await onesignalHelper.isSubscribed()
  4. When managing user identity

    • Need to set user ID or external user ID
    • Need to set user email or phone
    • Example: await onesignalHelper.setExternalUserId('user-123') and await onesignalHelper.setEmail('user@example.com')
  5. When managing user tags

    • Need to add/remove tags for segmentation
    • Need to set tags in bulk
    • Example: await onesignalHelper.addTags({ plan: 'premium', language: 'en' }) and await onesignalHelper.removeTags(['old-tag'])
  6. When handling notification events

    • Need to handle notification received/opened
    • Need to track user engagement
    • Example: onesignalHelper.onNotificationReceived((notification) => { ... }) and onesignalHelper.onNotificationOpened((result) => { ... })
  7. When implementing notification preferences

    • Need to allow users to enable/disable notifications
    • Need to sync preferences with OneSignal
    • Example: await onesignalHelper.setSubscription(userPreferences.pushNotifications)
  8. When implementing deep linking from notifications

    • Need to handle notification actions
    • Need to navigate based on notification data
    • Example: onesignalHelper.onNotificationOpened((result) => { navigateToScreen(result.notification.additionalData.screen) })

Mockups/Examples

Example 1: Basic Initialization

import { onesignalHelper } from 'masterfabric-expo-core';

// Initialize OneSignal on app start
await onesignalHelper.initialize('your-onesignal-app-id', {
  onNotificationReceived: (notification) => {
    console.log('Notification received:', notification);
  },
  onNotificationOpened: (result) => {
    console.log('Notification opened:', result);
    // Handle navigation based on notification data
  },
  onNotificationDismissed: (notification) => {
    console.log('Notification dismissed:', notification);
  }
});

Example 2: Permission Management (Using Permissions Handler)

import { onesignalHelper } from 'masterfabric-expo-core';
import { permissionsHandler } from 'masterfabric-expo-core';

// Request notification permission using permissions handler
const notificationStatus = await permissionsHandler.requestNotifications({
  alert: true,
  badge: true,
  sound: true
});

if (notificationStatus.granted) {
  console.log('Notification permission granted');
  // Now initialize OneSignal subscription
  await onesignalHelper.setSubscription(true);
} else if (notificationStatus.blocked) {
  console.log('Permission permanently denied');
  // Show settings alert using permissions handler
  permissionsHandler.showSettingsAlert({
    permission: 'notifications',
    title: 'Notification Permission Required',
    message: 'Please enable notifications in Settings to receive push notifications',
    openSettings: true
  });
} else {
  console.log('Permission denied, can ask again');
}

// Check current permission status via permissions handler
const hasPermission = await permissionsHandler.check('notifications');
console.log('Has permission:', hasPermission.granted);

// OneSignal helper also provides convenience methods that use permissions handler internally
const isSubscribed = await onesignalHelper.isSubscribed();
console.log('Is subscribed:', isSubscribed);

Example 3: Subscription Management

import { onesignalHelper } from 'masterfabric-expo-core';

// Check subscription status
const isSubscribed = await onesignalHelper.isSubscribed();
console.log('Is subscribed:', isSubscribed);

// Enable push notifications
await onesignalHelper.setSubscription(true);

// Disable push notifications
await onesignalHelper.setSubscription(false);

// Get push token/player ID
const playerId = await onesignalHelper.getPlayerId();
console.log('Player ID:', playerId);

// Listen for subscription changes
onesignalHelper.onSubscriptionChanged((isSubscribed) => {
  console.log('Subscription changed:', isSubscribed);
  // Update UI accordingly
});

Example 4: User Identity Management

import { onesignalHelper } from 'masterfabric-expo-core';

// Set external user ID (your app's user ID)
await onesignalHelper.setExternalUserId('user-123');

// Get external user ID
const externalUserId = await onesignalHelper.getExternalUserId();
console.log('External User ID:', externalUserId);

// Set user email
await onesignalHelper.setEmail('user@example.com');

// Set user phone number
await onesignalHelper.setPhoneNumber('+1234567890');

// Set user data (custom attributes)
await onesignalHelper.setUserData({
  name: 'John Doe',
  plan: 'premium',
  createdAt: '2024-01-01'
});

// Get user data
const userData = await onesignalHelper.getUserData();
console.log('User Data:', userData);

// Remove external user ID (logout)
await onesignalHelper.removeExternalUserId();

Example 5: Tag Management

import { onesignalHelper } from 'masterfabric-expo-core';

// Add single tag
await onesignalHelper.addTag('premium-user');

// Add multiple tags
await onesignalHelper.addTags({
  plan: 'premium',
  language: 'en',
  region: 'us',
  version: '2.0'
});

// Remove single tag
await onesignalHelper.removeTag('old-tag');

// Remove multiple tags
await onesignalHelper.removeTags(['tag1', 'tag2']);

// Get all tags
const tags = await onesignalHelper.getTags();
console.log('Tags:', tags);
// { plan: 'premium', language: 'en', region: 'us' }

// Set tags (replaces all existing tags)
await onesignalHelper.setTags({
  plan: 'free',
  language: 'tr'
});

// Clear all tags
await onesignalHelper.clearTags();

Example 6: Notification Event Handling

import { onesignalHelper } from 'masterfabric-expo-core';
import { useNavigation } from '@react-navigation/native';

const MyComponent = () => {
  const navigation = useNavigation();

  useEffect(() => {
    // Handle notification received (foreground)
    const unsubscribeReceived = onesignalHelper.onNotificationReceived((notification) => {
      console.log('Notification received:', notification);
      
      // Show local notification or update badge
      if (notification.body) {
        // Handle notification display
      }
    });

    // Handle notification opened
    const unsubscribeOpened = onesignalHelper.onNotificationOpened((result) => {
      console.log('Notification opened:', result);
      
      const { notification } = result;
      const additionalData = notification.additionalData;
      
      // Navigate based on notification data
      if (additionalData?.screen) {
        navigation.navigate(additionalData.screen, additionalData.params);
      }
    });

    // Handle notification dismissed
    const unsubscribeDismissed = onesignalHelper.onNotificationDismissed((notification) => {
      console.log('Notification dismissed:', notification);
      // Track dismissal analytics
    });

    // Cleanup
    return () => {
      unsubscribeReceived();
      unsubscribeOpened();
      unsubscribeDismissed();
    };
  }, [navigation]);
};

Example 7: Complete App Initialization (With Permissions Handler)

import { onesignalHelper, permissionsHandler } from 'masterfabric-expo-core';
import { useEffect } from 'react';
import { useAppStore } from '@/src/shared/store';

export default function App() {
  const { user, setUser } = useAppStore();

  useEffect(() => {
    // Initialize OneSignal
    onesignalHelper.initialize('your-onesignal-app-id', {
      onNotificationReceived: (notification) => {
        console.log('Notification received:', notification);
        // Update badge count, show in-app notification, etc.
      },
      onNotificationOpened: (result) => {
        console.log('Notification opened:', result);
        const { notification } = result;
        const additionalData = notification.additionalData;
        
        // Handle deep linking
        if (additionalData?.screen) {
          // Navigate to screen
        }
      },
      onNotificationDismissed: (notification) => {
        console.log('Notification dismissed:', notification);
      }
    }).then(async () => {
      // Request notification permission using permissions handler
      const notificationStatus = await permissionsHandler.requestNotifications({
        alert: true,
        badge: true,
        sound: true,
        rationale: 'We need notification permission to send you important updates'
      });
      
      if (notificationStatus.granted) {
        // Enable OneSignal subscription
        await onesignalHelper.setSubscription(true);
        
        if (user) {
          // Set user identity
          await onesignalHelper.setExternalUserId(user.id);
          await onesignalHelper.setEmail(user.email);
          
          // Set user tags
          await onesignalHelper.addTags({
            plan: user.plan,
            language: user.language,
            version: '2.0'
          });
        }
      } else if (notificationStatus.blocked) {
        // Permission permanently denied, show settings alert
        permissionsHandler.showSettingsAlert({
          permission: 'notifications',
          title: 'Notifications Disabled',
          message: 'Please enable notifications in Settings to receive updates',
          openSettings: true
        });
      }
    });
  }, [user]);
}

Example 8: Settings Screen Integration (With Permissions Handler)

import { onesignalHelper, permissionsHandler } from 'masterfabric-expo-core';
import { useState, useEffect } from 'react';

const NotificationSettingsScreen = () => {
  const [isSubscribed, setIsSubscribed] = useState(false);
  const [hasPermission, setHasPermission] = useState(false);

  useEffect(() => {
    // Check current permission status using permissions handler
    permissionsHandler.check('notifications').then((status) => {
      setHasPermission(status.granted);
      // Check OneSignal subscription status
      onesignalHelper.isSubscribed().then(setIsSubscribed);
    });
  }, []);

  const handleToggleNotifications = async (enabled: boolean) => {
    if (enabled && !hasPermission) {
      // Request permission using permissions handler
      const notificationStatus = await permissionsHandler.requestNotifications({
        alert: true,
        badge: true,
        sound: true,
        rationale: 'Enable notifications to receive important updates'
      });
      
      if (!notificationStatus.granted) {
        if (notificationStatus.blocked) {
          // Show settings alert
          permissionsHandler.showSettingsAlert({
            permission: 'notifications',
            title: 'Permission Required',
            message: 'Please enable notifications in Settings',
            openSettings: true
          });
        }
        return;
      }
      setHasPermission(true);
    }
    
    // Enable/disable OneSignal subscription
    await onesignalHelper.setSubscription(enabled);
    setIsSubscribed(enabled);
  };

  return (
    <View>
      <Switch
        value={isSubscribed}
        onValueChange={handleToggleNotifications}
      />
      <Text>Push Notifications</Text>
      {!hasPermission && (
        <Text style={styles.hint}>
          Notification permission is required to enable push notifications
        </Text>
      )}
    </View>
  );
};

Example 9: Deep Linking from Notifications

import { onesignalHelper } from 'masterfabric-expo-core';
import { useNavigation } from '@react-navigation/native';

const useNotificationDeepLinking = () => {
  const navigation = useNavigation();

  useEffect(() => {
    const unsubscribe = onesignalHelper.onNotificationOpened((result) => {
      const { notification } = result;
      const additionalData = notification.additionalData || {};
      
      // Handle different notification types
      switch (additionalData.type) {
        case 'profile':
          navigation.navigate('Profile', { userId: additionalData.userId });
          break;
        case 'message':
          navigation.navigate('Messages', { conversationId: additionalData.conversationId });
          break;
        case 'order':
          navigation.navigate('OrderDetails', { orderId: additionalData.orderId });
          break;
        default:
          navigation.navigate('Home');
      }
    });

    return unsubscribe;
  }, [navigation]);
};

Example 10: User Login/Logout Integration

import { onesignalHelper } from 'masterfabric-expo-core';

// On user login
const handleLogin = async (user: User) => {
  // Set user identity in OneSignal
  await onesignalHelper.setExternalUserId(user.id);
  await onesignalHelper.setEmail(user.email);
  await onesignalHelper.setPhoneNumber(user.phone);
  
  // Set user tags
  await onesignalHelper.addTags({
    plan: user.subscriptionPlan,
    language: user.preferredLanguage,
    region: user.region,
    signupDate: user.createdAt
  });
  
  // Enable notifications if user has permission
  const hasPermission = await onesignalHelper.hasPermission();
  if (hasPermission) {
    await onesignalHelper.setSubscription(true);
  }
};

// On user logout
const handleLogout = async () => {
  // Remove external user ID
  await onesignalHelper.removeExternalUserId();
  
  // Clear user-specific tags (keep app-level tags if needed)
  await onesignalHelper.removeTags(['plan', 'language', 'region']);
  
  // Optionally disable notifications
  // await onesignalHelper.setSubscription(false);
};

Example 11: In-App Message Handling

import { onesignalHelper } from 'masterfabric-expo-core';

// Handle in-app message displayed
onesignalHelper.onInAppMessageDisplayed((message) => {
  console.log('In-app message displayed:', message);
  // Track analytics
});

// Handle in-app message action
onesignalHelper.onInAppMessageAction((action) => {
  console.log('In-app message action:', action);
  const { clickName, clickUrl } = action;
  
  // Handle action based on clickName or clickUrl
  if (clickName === 'open_settings') {
    // Navigate to settings
  } else if (clickUrl) {
    // Open URL
  }
});

Example 12: Notification Statistics

import { onesignalHelper } from 'masterfabric-expo-core';

// Get notification statistics
const stats = await onesignalHelper.getNotificationStats();
console.log('Notification stats:', stats);
// {
//   totalReceived: 10,
//   totalOpened: 7,
//   totalDismissed: 3,
//   openRate: 0.7
// }

// Track notification event
onesignalHelper.trackEvent('notification_received', {
  notificationId: '123',
  title: 'Test Notification'
});

Implementation Ideas

File Structure

packages/masterfabric-expo-core/src/helpers/
  └── onesignal_helper.ts

Proposed Functions

Initialization Functions

  • initialize(appId: string, options?: OneSignalOptions): Promise<void> - Initialize OneSignal
  • isInitialized(): boolean - Check if OneSignal is initialized
  • setAppId(appId: string): void - Set OneSignal app ID

Permission Functions

  • Note: OneSignal helper uses permissionsHandler helper for all permission operations
  • requestPermission(): Promise<boolean> - Request notification permission (uses permissionsHandler.requestNotifications() internally)
  • hasPermission(): Promise<boolean> - Check if permission is granted (uses permissionsHandler.check('notifications') internally)
  • getPermissionStatus(): Promise<PermissionStatus> - Get detailed permission status (uses permissions handler)

Subscription Functions

  • isSubscribed(): Promise<boolean> - Check subscription status
  • setSubscription(enabled: boolean): Promise<void> - Enable/disable push notifications
  • getPlayerId(): Promise<string | null> - Get OneSignal player ID
  • onSubscriptionChanged(callback: (isSubscribed: boolean) => void): () => void - Listen for subscription changes

User Identity Functions

  • setExternalUserId(userId: string): Promise<void> - Set external user ID
  • getExternalUserId(): Promise<string | null> - Get external user ID
  • removeExternalUserId(): Promise<void> - Remove external user ID
  • setEmail(email: string): Promise<void> - Set user email
  • setPhoneNumber(phone: string): Promise<void> - Set user phone number
  • setUserData(data: Record<string, any>): Promise<void> - Set user data/attributes
  • getUserData(): Promise<Record<string, any>> - Get user data

Tag Functions

  • addTag(key: string, value?: string): Promise<void> - Add single tag
  • addTags(tags: Record<string, string>): Promise<void> - Add multiple tags
  • removeTag(key: string): Promise<void> - Remove single tag
  • removeTags(keys: string[]): Promise<void> - Remove multiple tags
  • getTags(): Promise<Record<string, string>> - Get all tags
  • setTags(tags: Record<string, string>): Promise<void> - Set tags (replace all)
  • clearTags(): Promise<void> - Clear all tags

Event Handler Functions

  • onNotificationReceived(callback: (notification: Notification) => void): () => void - Handle notification received
  • onNotificationOpened(callback: (result: NotificationOpenedResult) => void): () => void - Handle notification opened
  • onNotificationDismissed(callback: (notification: Notification) => void): () => void - Handle notification dismissed
  • onInAppMessageDisplayed(callback: (message: InAppMessage) => void): () => void - Handle in-app message displayed
  • onInAppMessageAction(callback: (action: InAppMessageAction) => void): () => void - Handle in-app message action

Utility Functions

  • getNotificationStats(): Promise<NotificationStats> - Get notification statistics
  • trackEvent(eventName: string, data?: Record<string, any>): void - Track custom event
  • promptForPushNotifications(): Promise<boolean> - Prompt for push notifications (iOS)

Type Definitions

export interface OneSignalOptions {
  onNotificationReceived?: (notification: Notification) => void;
  onNotificationOpened?: (result: NotificationOpenedResult) => void;
  onNotificationDismissed?: (notification: Notification) => void;
  onInAppMessageDisplayed?: (message: InAppMessage) => void;
  onInAppMessageAction?: (action: InAppMessageAction) => void;
  autoRegister?: boolean;
  requiresUserPrivacyConsent?: boolean; // iOS
}

export interface Notification {
  notificationId: string;
  title?: string;
  body?: string;
  additionalData?: Record<string, any>;
  launchURL?: string;
  sound?: string;
  badge?: number;
  actionButtons?: NotificationActionButton[];
}

export interface NotificationOpenedResult {
  notification: Notification;
  action?: NotificationAction;
}

export interface NotificationAction {
  actionId?: string;
  type: 'opened' | 'action';
}

export interface NotificationActionButton {
  id: string;
  text: string;
  icon?: string;
}

export interface InAppMessage {
  messageId: string;
  name?: string;
  htmlContent?: string;
  jsonContent?: any;
}

export interface InAppMessageAction {
  clickName?: string;
  clickUrl?: string;
  firstClick: boolean;
  closesMessage: boolean;
}

export interface PermissionStatus {
  status: 'granted' | 'denied' | 'notDetermined' | 'ephemeral';
  canRequest: boolean;
}

export interface NotificationStats {
  totalReceived: number;
  totalOpened: number;
  totalDismissed: number;
  openRate: number;
}

export interface NotificationReceivedEvent {
  notification: Notification;
  isAppInFocus: boolean;
}

Implementation Pattern

Following the existing helper patterns (like toast_helper.ts, url_launcher_helper.ts):

  1. Class-based API with singleton instance
  2. Include comprehensive JSDoc comments with examples
  3. Type-safe with TypeScript
  4. Use OneSignal React Native SDK (react-native-onesignal)
  5. Export from index.ts for easy importing
  6. Handle platform-specific differences gracefully
  7. Error handling with try-catch and proper error messages
  8. Event listener management with cleanup functions

Example Implementation Structure

/**
 * OneSignal Helper
 * 
 * Provides a unified API for OneSignal push notification integration,
 * user management, tags, and notification event handling.
 * 
 * @example
 * ```typescript
 * import { onesignalHelper } from 'masterfabric-expo-core';
 * 
 * // Initialize OneSignal
 * await onesignalHelper.initialize('your-app-id', {
 *   onNotificationReceived: (notification) => {
 *     console.log('Notification received:', notification);
 *   },
 *   onNotificationOpened: (result) => {
 *     console.log('Notification opened:', result);
 *   }
 * });
 * 
 * // Request permission
 * const granted = await onesignalHelper.requestPermission();
 * 
 * // Set user identity
 * await onesignalHelper.setExternalUserId('user-123');
 * await onesignalHelper.addTags({ plan: 'premium' });
 * ```
 */

import OneSignal from 'react-native-onesignal';
import { Platform } from 'react-native';

// Type exports
export type {
  OneSignalOptions,
  Notification,
  NotificationOpenedResult,
  NotificationAction,
  NotificationActionButton,
  InAppMessage,
  InAppMessageAction,
  PermissionStatus,
  NotificationStats,
  NotificationReceivedEvent,
};

// OneSignal Helper Class
class OneSignalHelper {
  private isInitializedFlag: boolean = false;
  private appId: string | null = null;
  private eventListeners: Map<string, Set<Function>> = new Map();
  private notificationStats: NotificationStats = {
    totalReceived: 0,
    totalOpened: 0,
    totalDismissed: 0,
    openRate: 0,
  };

  /**
   * Initialize OneSignal
   */
  async initialize(appId: string, options?: OneSignalOptions): Promise<void> {
    if (this.isInitializedFlag) {
      console.warn('OneSignal is already initialized');
      return;
    }

    try {
      this.appId = appId;
      
      // Set App ID
      OneSignal.setAppId(appId);

      // Set up event handlers
      if (options?.onNotificationReceived) {
        this.onNotificationReceived(options.onNotificationReceived);
      }

      if (options?.onNotificationOpened) {
        this.onNotificationOpened(options.onNotificationOpened);
      }

      if (options?.onNotificationDismissed) {
        this.onNotificationDismissed(options.onNotificationDismissed);
      }

      if (options?.onInAppMessageDisplayed) {
        this.onInAppMessageDisplayed(options.onInAppMessageDisplayed);
      }

      if (options?.onInAppMessageAction) {
        this.onInAppMessageAction(options.onInAppMessageAction);
      }

      // Set requires user privacy consent (iOS)
      if (options?.requiresUserPrivacyConsent !== undefined) {
        OneSignal.setRequiresUserPrivacyConsent(options.requiresUserPrivacyConsent);
      }

      // Enable automatic prompt (if not disabled)
      if (options?.autoRegister !== false) {
        OneSignal.promptForPushNotificationsWithUserResponse((response) => {
          console.log('Prompt response:', response);
        });
      }

      this.isInitializedFlag = true;
    } catch (error) {
      console.error('Error initializing OneSignal:', error);
      throw error;
    }
  }

  /**
   * Check if OneSignal is initialized
   */
  isInitialized(): boolean {
    return this.isInitializedFlag;
  }

  /**
   * Request notification permission
   * Uses permissionsHandler helper for unified permission management
   */
  async requestPermission(): Promise<boolean> {
    try {
      // Import permissions handler dynamically to avoid circular dependencies
      const { permissionsHandler } = await import('./permissions_handler_helper');
      
      const notificationStatus = await permissionsHandler.requestNotifications({
        alert: true,
        badge: true,
        sound: true
      });
      
      return notificationStatus.granted;
    } catch (error) {
      console.error('Error requesting permission:', error);
      // Fallback to OneSignal's native prompt if permissions handler is not available
      return new Promise((resolve) => {
        OneSignal.promptForPushNotificationsWithUserResponse((response) => {
          resolve(response);
        });
      });
    }
  }

  /**
   * Check if permission is granted
   * Uses permissionsHandler helper for unified permission checking
   */
  async hasPermission(): Promise<boolean> {
    try {
      // Import permissions handler dynamically to avoid circular dependencies
      const { permissionsHandler } = await import('./permissions_handler_helper');
      
      const status = await permissionsHandler.check('notifications');
      return status.granted;
    } catch (error) {
      console.error('Error checking permission:', error);
      // Fallback to OneSignal's device state if permissions handler is not available
      try {
        const deviceState = await OneSignal.getDeviceState();
        return deviceState?.isSubscribed ?? false;
      } catch (fallbackError) {
        return false;
      }
    }
  }

  /**
   * Get detailed permission status
   * Uses permissionsHandler helper for unified permission status
   */
  async getPermissionStatus(): Promise<PermissionStatus> {
    try {
      // Import permissions handler dynamically to avoid circular dependencies
      const { permissionsHandler } = await import('./permissions_handler_helper');
      
      const status = await permissionsHandler.check('notifications');
      
      return {
        status: status.status === 'granted' ? 'granted' : 
                status.status === 'blocked' ? 'denied' : 
                status.status === 'denied' ? 'denied' : 'notDetermined',
        canRequest: status.canAskAgain,
      };
    } catch (error) {
      console.error('Error getting permission status:', error);
      // Fallback to OneSignal's device state if permissions handler is not available
      try {
        const deviceState = await OneSignal.getDeviceState();
        const hasPermission = deviceState?.isSubscribed ?? false;
        
        return {
          status: hasPermission ? 'granted' : 'denied',
          canRequest: !hasPermission,
        };
      } catch (fallbackError) {
        return {
          status: 'notDetermined',
          canRequest: true,
        };
      }
    }
  }

  /**
   * Check subscription status
   */
  async isSubscribed(): Promise<boolean> {
    try {
      const deviceState = await OneSignal.getDeviceState();
      return deviceState?.isSubscribed ?? false;
    } catch (error) {
      console.error('Error checking subscription:', error);
      return false;
    }
  }

  /**
   * Enable/disable push notifications
   */
  async setSubscription(enabled: boolean): Promise<void> {
    try {
      OneSignal.disablePush(!enabled);
    } catch (error) {
      console.error('Error setting subscription:', error);
      throw error;
    }
  }

  /**
   * Get OneSignal player ID
   */
  async getPlayerId(): Promise<string | null> {
    try {
      const deviceState = await OneSignal.getDeviceState();
      return deviceState?.userId ?? null;
    } catch (error) {
      console.error('Error getting player ID:', error);
      return null;
    }
  }

  /**
   * Listen for subscription changes
   */
  onSubscriptionChanged(callback: (isSubscribed: boolean) => void): () => void {
    const listener = OneSignal.addSubscriptionObserver((event) => {
      callback(event.to.isSubscribed);
    });

    return () => {
      listener.remove();
    };
  }

  /**
   * Set external user ID
   */
  async setExternalUserId(userId: string): Promise<void> {
    try {
      await OneSignal.setExternalUserId(userId);
    } catch (error) {
      console.error('Error setting external user ID:', error);
      throw error;
    }
  }

  /**
   * Get external user ID
   */
  async getExternalUserId(): Promise<string | null> {
    try {
      const deviceState = await OneSignal.getDeviceState();
      return deviceState?.externalUserId ?? null;
    } catch (error) {
      console.error('Error getting external user ID:', error);
      return null;
    }
  }

  /**
   * Remove external user ID
   */
  async removeExternalUserId(): Promise<void> {
    try {
      await OneSignal.removeExternalUserId();
    } catch (error) {
      console.error('Error removing external user ID:', error);
      throw error;
    }
  }

  /**
   * Set user email
   */
  async setEmail(email: string): Promise<void> {
    try {
      await OneSignal.setEmail(email);
    } catch (error) {
      console.error('Error setting email:', error);
      throw error;
    }
  }

  /**
   * Set user phone number
   */
  async setPhoneNumber(phone: string): Promise<void> {
    try {
      await OneSignal.setSMSNumber(phone);
    } catch (error) {
      console.error('Error setting phone number:', error);
      throw error;
    }
  }

  /**
   * Set user data/attributes
   */
  async setUserData(data: Record<string, any>): Promise<void> {
    try {
      // OneSignal uses tags for user data
      await OneSignal.sendTags(data);
    } catch (error) {
      console.error('Error setting user data:', error);
      throw error;
    }
  }

  /**
   * Get user data
   */
  async getUserData(): Promise<Record<string, any>> {
    try {
      const deviceState = await OneSignal.getDeviceState();
      return deviceState?.tags ?? {};
    } catch (error) {
      console.error('Error getting user data:', error);
      return {};
    }
  }

  /**
   * Add single tag
   */
  async addTag(key: string, value: string = 'true'): Promise<void> {
    try {
      await OneSignal.sendTag(key, value);
    } catch (error) {
      console.error('Error adding tag:', error);
      throw error;
    }
  }

  /**
   * Add multiple tags
   */
  async addTags(tags: Record<string, string>): Promise<void> {
    try {
      await OneSignal.sendTags(tags);
    } catch (error) {
      console.error('Error adding tags:', error);
      throw error;
    }
  }

  /**
   * Remove single tag
   */
  async removeTag(key: string): Promise<void> {
    try {
      await OneSignal.deleteTag(key);
    } catch (error) {
      console.error('Error removing tag:', error);
      throw error;
    }
  }

  /**
   * Remove multiple tags
   */
  async removeTags(keys: string[]): Promise<void> {
    try {
      await OneSignal.deleteTags(keys);
    } catch (error) {
      console.error('Error removing tags:', error);
      throw error;
    }
  }

  /**
   * Get all tags
   */
  async getTags(): Promise<Record<string, string>> {
    try {
      const deviceState = await OneSignal.getDeviceState();
      return deviceState?.tags ?? {};
    } catch (error) {
      console.error('Error getting tags:', error);
      return {};
    }
  }

  /**
   * Set tags (replace all)
   */
  async setTags(tags: Record<string, string>): Promise<void> {
    try {
      // Get current tags
      const currentTags = await this.getTags();
      
      // Remove all current tags
      const currentKeys = Object.keys(currentTags);
      if (currentKeys.length > 0) {
        await OneSignal.deleteTags(currentKeys);
      }
      
      // Set new tags
      await OneSignal.sendTags(tags);
    } catch (error) {
      console.error('Error setting tags:', error);
      throw error;
    }
  }

  /**
   * Clear all tags
   */
  async clearTags(): Promise<void> {
    try {
      const tags = await this.getTags();
      const keys = Object.keys(tags);
      if (keys.length > 0) {
        await OneSignal.deleteTags(keys);
      }
    } catch (error) {
      console.error('Error clearing tags:', error);
      throw error;
    }
  }

  /**
   * Handle notification received
   */
  onNotificationReceived(callback: (notification: Notification) => void): () => void {
    const listener = OneSignal.setNotificationWillShowInForegroundHandler((event) => {
      const notification = event.getNotification();
      this.notificationStats.totalReceived++;
      this.updateOpenRate();
      
      callback({
        notificationId: notification.notificationId,
        title: notification.title,
        body: notification.body,
        additionalData: notification.additionalData,
        launchURL: notification.launchURL,
        sound: notification.sound,
        badge: notification.badge,
      });
    });

    return () => {
      // Cleanup if needed
    };
  }

  /**
   * Handle notification opened
   */
  onNotificationOpened(callback: (result: NotificationOpenedResult) => void): () => void {
    const listener = OneSignal.setNotificationOpenedHandler((result) => {
      const notification = result.notification;
      this.notificationStats.totalOpened++;
      this.updateOpenRate();
      
      callback({
        notification: {
          notificationId: notification.notificationId,
          title: notification.title,
          body: notification.body,
          additionalData: notification.additionalData,
          launchURL: notification.launchURL,
        },
        action: result.action ? {
          actionId: result.action.actionID,
          type: result.action.type === 0 ? 'opened' : 'action',
        } : undefined,
      });
    });

    return () => {
      // Cleanup if needed
    };
  }

  /**
   * Handle notification dismissed
   */
  onNotificationDismissed(callback: (notification: Notification) => void): () => void {
    // OneSignal doesn't have a direct dismissed handler
    // This would need to be implemented with local tracking
    // For now, return a no-op cleanup function
    return () => {};
  }

  /**
   * Handle in-app message displayed
   */
  onInAppMessageDisplayed(callback: (message: InAppMessage) => void): () => void {
    const listener = OneSignal.setInAppMessageClickHandler((event) => {
      // This is actually for clicks, not display
      // OneSignal doesn't have a direct display handler
    });

    return () => {
      // Cleanup if needed
    };
  }

  /**
   * Handle in-app message action
   */
  onInAppMessageAction(callback: (action: InAppMessageAction) => void): () => void {
    const listener = OneSignal.setInAppMessageClickHandler((event) => {
      callback({
        clickName: event.result.actionId,
        clickUrl: event.result.url,
        firstClick: true,
        closesMessage: event.result.closingMessage,
      });
    });

    return () => {
      // Cleanup if needed
    };
  }

  /**
   * Get notification statistics
   */
  async getNotificationStats(): Promise<NotificationStats> {
    return { ...this.notificationStats };
  }

  /**
   * Track custom event
   */
  trackEvent(eventName: string, data?: Record<string, any>): void {
    try {
      OneSignal.sendOutcome(eventName, data);
    } catch (error) {
      console.error('Error tracking event:', error);
    }
  }

  /**
   * Update open rate
   */
  private updateOpenRate(): void {
    if (this.notificationStats.totalReceived > 0) {
      this.notificationStats.openRate = 
        this.notificationStats.totalOpened / this.notificationStats.totalReceived;
    }
  }
}

// Export singleton instance
export const onesignalHelper = new OneSignalHelper();

// Export class for advanced usage
export { OneSignalHelper };

Integration with index.ts

// packages/masterfabric-expo-core/src/helpers/index.ts

// ... existing exports ...

// OneSignal Helper
export * from './onesignal_helper';

Screen Implementation Structure

src/screens/onesignal-helper/
├── components/
│   ├── onesignal-helper-screen.tsx
│   ├── permission-status-card.tsx
│   ├── subscription-status-card.tsx
│   ├── tag-manager.tsx
│   ├── notification-test-card.tsx
│   └── event-log.tsx
├── hooks/
│   ├── use-onesignal-helper-view-model.ts
│   └── use-onesignal.ts
├── models/
│   └── onesignal-helper.models.ts
├── store/
│   └── onesignal-helper-store.ts
├── styles/
│   ├── onesignal-helper-screen.styles.ts
│   ├── permission-status-card.styles.ts
│   ├── subscription-status-card.styles.ts
│   ├── tag-manager.styles.ts
│   ├── notification-test-card.styles.ts
│   └── event-log.styles.ts
├── utils/
│   └── onesignal-helper-utils.ts
├── index.ts
└── README.md

Testing Interface Features

The testing interface should include:

  1. Initialization Section - Input for OneSignal App ID and initialize button
  2. Permission Status Card - Display current permission status with request button
  3. Subscription Status Card - Display subscription status with enable/disable toggle
  4. User Identity Section - Inputs for external user ID, email, phone number
  5. Tag Manager - Add/remove tags interface with tag list
  6. Notification Test Card - Test notification sending (requires server-side setup)
  7. Event Log - Display notification events (received, opened, dismissed)
  8. Statistics Display - Show notification statistics (received, opened, open rate)

Contribution

  • I would like to work on this feature
  • I can help with testing
  • I can help with documentation
  • I can provide feedback during development

Additional Context

Related Helpers

This helper should integrate well with existing helpers:

  • permissions_handler_helper.ts - REQUIRED: OneSignal helper uses permissions handler for all notification permission operations. This ensures unified permission management across the app and proper handling of permission states (granted, denied, blocked).
  • logger_helper.ts - For logging OneSignal operations and errors
  • toast_helper.ts - For showing notification status to users
  • snackbar_helper.ts - For showing operation status
  • url_launcher_helper.ts - For deep linking from notifications

Integration with Permissions Handler

Important: OneSignal helper is designed to work with permissions_handler_helper.ts. All notification permission operations should go through the permissions handler for:

  1. Unified Permission Management: Consistent permission handling across the app
  2. Platform-Specific Handling: Proper iOS/Android permission differences
  3. Permission State Management: Better handling of permission states (granted, denied, blocked, limited)
  4. Settings Redirect: Automatic handling of permanently denied permissions with settings redirect
  5. Permission Rationale: Support for showing permission rationale to users
  6. Permission Status Tracking: Unified permission status tracking across the app

Usage Pattern:

// Always use permissions handler for notification permissions
const status = await permissionsHandler.requestNotifications({ ... });

// Then use OneSignal helper for subscription management
if (status.granted) {
  await onesignalHelper.setSubscription(true);
}

Common Use Cases in MasterFabric

  • Push notification setup on app start
  • Notification permission requests
  • User subscription management in settings
  • Tag-based user segmentation
  • Deep linking from notifications
  • Notification analytics and tracking
  • In-app message handling

Testing Considerations

  • Unit tests for all helper functions
  • Integration tests with OneSignal SDK
  • Platform-specific tests (iOS vs Android)
  • Permission flow tests
  • Event handler tests
  • Tag management tests
  • Error handling tests
  • Edge cases: network errors, SDK initialization failures

Documentation Needs

  • API documentation with examples for each function
  • OneSignal setup guide
  • Permission handling guide
  • Tag management guide
  • Event handling guide
  • Deep linking guide
  • Best practices for push notifications
  • Testing interface documentation

Dependencies

  • react-native-onesignal - OneSignal React Native SDK
  • react-native - For platform detection
  • permissions_handler_helper.ts - REQUIRED: For notification permission management
  • No additional dependencies needed

Compatibility

  • React Native OneSignal SDK
  • Should work with both iOS and Android
  • Web platform support (with limitations)
  • Compatible with Expo (may require custom native code)

Security Considerations

  • Secure storage of OneSignal App ID
  • Validate user input for tags and user data
  • Handle sensitive user data (email, phone) securely
  • Prevent tag injection attacks
  • Secure external user ID handling

Platform-Specific Notes

iOS

  • Requires notification permissions (handled by permissionsHandler.requestNotifications())
  • Requires user privacy consent (GDPR)
  • Supports badge management
  • Supports notification actions
  • Requires proper entitlements in Xcode
  • Permission handling: Use permissionsHandler for iOS-specific permission states (provisional, etc.)

Android

  • Requires notification permissions (Android 13+) - handled by permissionsHandler.requestNotifications()
  • Supports notification channels
  • Supports notification actions
  • Requires proper permissions in AndroidManifest.xml
  • Permission handling: Use permissionsHandler for Android-specific permission rationale

Web

  • Limited support
  • Requires OneSignal Web SDK
  • Different initialization process

OneSignal Setup Requirements

  1. OneSignal Account - Create account at onesignal.com
  2. App Registration - Register iOS and Android apps
  3. App IDs - Get OneSignal App IDs for each platform
  4. iOS Setup - Configure APNs certificates/keys
  5. Android Setup - Configure FCM server key
  6. SDK Installation - Install react-native-onesignal package

Future Enhancements (Optional)

  • Support for notification scheduling
  • Support for notification templates
  • Support for A/B testing
  • Support for notification analytics dashboard
  • Support for notification preview
  • Support for notification history
  • Support for notification grouping
  • Support for rich notifications (images, actions)
  • Support for notification sounds customization
  • Support for notification badge management

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions