-
Notifications
You must be signed in to change notification settings - Fork 5
Description
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
permissionsHandlerhelper 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:
-
Initialization Functions
- Initialize OneSignal with app ID
- Configure notification handlers
- Set up event listeners
-
Permission Management
- Uses
permissionsHandlerhelper 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
- Uses
-
User Subscription Management
- Get subscription status
- Enable/disable push notifications
- Get push token/player ID
- Handle subscription state changes
-
User ID Management
- Set/get user ID
- Set/get external user ID
- Set user email
- Set user phone number
- Set user data (custom attributes)
-
Tag Management
- Add/remove tags
- Get all tags
- Set tags in bulk
- Clear all tags
-
Notification Event Handling
- Handle notification received
- Handle notification opened
- Handle notification dismissed
- Handle in-app message displayed
- Handle in-app message action
-
Notification Display
- Show notification programmatically
- Customize notification appearance
- Handle notification actions
-
Analytics & Tracking
- Track notification events
- Get notification statistics
- Track user engagement
Alternatives Considered
-
Using OneSignal SDK directly without helpers
- Too much boilerplate code
- Inconsistent implementations
- Poor developer experience
- Manual event handling setup
-
Using other push notification services
- Firebase Cloud Messaging (FCM)
- Expo Notifications
- AWS SNS
- But OneSignal provides better features (tags, segments, analytics)
-
Creating project-specific helpers
- Code duplication
- Inconsistent implementations
- Maintenance burden
- Security vulnerabilities
Use Cases
-
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 })
-
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 })thenawait onesignalHelper.setSubscription(status.granted)
- Need to request permissions from user using
-
When managing user subscription
- Need to enable/disable push notifications
- Need to check subscription status
- Example:
await onesignalHelper.setSubscription(true)andconst isSubscribed = await onesignalHelper.isSubscribed()
-
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')andawait onesignalHelper.setEmail('user@example.com')
-
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' })andawait onesignalHelper.removeTags(['old-tag'])
-
When handling notification events
- Need to handle notification received/opened
- Need to track user engagement
- Example:
onesignalHelper.onNotificationReceived((notification) => { ... })andonesignalHelper.onNotificationOpened((result) => { ... })
-
When implementing notification preferences
- Need to allow users to enable/disable notifications
- Need to sync preferences with OneSignal
- Example:
await onesignalHelper.setSubscription(userPreferences.pushNotifications)
-
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 OneSignalisInitialized(): boolean- Check if OneSignal is initializedsetAppId(appId: string): void- Set OneSignal app ID
Permission Functions
- Note: OneSignal helper uses
permissionsHandlerhelper for all permission operations requestPermission(): Promise<boolean>- Request notification permission (usespermissionsHandler.requestNotifications()internally)hasPermission(): Promise<boolean>- Check if permission is granted (usespermissionsHandler.check('notifications')internally)getPermissionStatus(): Promise<PermissionStatus>- Get detailed permission status (uses permissions handler)
Subscription Functions
isSubscribed(): Promise<boolean>- Check subscription statussetSubscription(enabled: boolean): Promise<void>- Enable/disable push notificationsgetPlayerId(): Promise<string | null>- Get OneSignal player IDonSubscriptionChanged(callback: (isSubscribed: boolean) => void): () => void- Listen for subscription changes
User Identity Functions
setExternalUserId(userId: string): Promise<void>- Set external user IDgetExternalUserId(): Promise<string | null>- Get external user IDremoveExternalUserId(): Promise<void>- Remove external user IDsetEmail(email: string): Promise<void>- Set user emailsetPhoneNumber(phone: string): Promise<void>- Set user phone numbersetUserData(data: Record<string, any>): Promise<void>- Set user data/attributesgetUserData(): Promise<Record<string, any>>- Get user data
Tag Functions
addTag(key: string, value?: string): Promise<void>- Add single tagaddTags(tags: Record<string, string>): Promise<void>- Add multiple tagsremoveTag(key: string): Promise<void>- Remove single tagremoveTags(keys: string[]): Promise<void>- Remove multiple tagsgetTags(): Promise<Record<string, string>>- Get all tagssetTags(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 receivedonNotificationOpened(callback: (result: NotificationOpenedResult) => void): () => void- Handle notification openedonNotificationDismissed(callback: (notification: Notification) => void): () => void- Handle notification dismissedonInAppMessageDisplayed(callback: (message: InAppMessage) => void): () => void- Handle in-app message displayedonInAppMessageAction(callback: (action: InAppMessageAction) => void): () => void- Handle in-app message action
Utility Functions
getNotificationStats(): Promise<NotificationStats>- Get notification statisticstrackEvent(eventName: string, data?: Record<string, any>): void- Track custom eventpromptForPushNotifications(): 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):
- Class-based API with singleton instance
- Include comprehensive JSDoc comments with examples
- Type-safe with TypeScript
- Use OneSignal React Native SDK (
react-native-onesignal) - Export from
index.tsfor easy importing - Handle platform-specific differences gracefully
- Error handling with try-catch and proper error messages
- 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:
- Initialization Section - Input for OneSignal App ID and initialize button
- Permission Status Card - Display current permission status with request button
- Subscription Status Card - Display subscription status with enable/disable toggle
- User Identity Section - Inputs for external user ID, email, phone number
- Tag Manager - Add/remove tags interface with tag list
- Notification Test Card - Test notification sending (requires server-side setup)
- Event Log - Display notification events (received, opened, dismissed)
- 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 errorstoast_helper.ts- For showing notification status to userssnackbar_helper.ts- For showing operation statusurl_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:
- Unified Permission Management: Consistent permission handling across the app
- Platform-Specific Handling: Proper iOS/Android permission differences
- Permission State Management: Better handling of permission states (granted, denied, blocked, limited)
- Settings Redirect: Automatic handling of permanently denied permissions with settings redirect
- Permission Rationale: Support for showing permission rationale to users
- 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 SDKreact-native- For platform detectionpermissions_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
permissionsHandlerfor 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
permissionsHandlerfor Android-specific permission rationale
Web
- Limited support
- Requires OneSignal Web SDK
- Different initialization process
OneSignal Setup Requirements
- OneSignal Account - Create account at onesignal.com
- App Registration - Register iOS and Android apps
- App IDs - Get OneSignal App IDs for each platform
- iOS Setup - Configure APNs certificates/keys
- Android Setup - Configure FCM server key
- SDK Installation - Install
react-native-onesignalpackage
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