Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ Use the `OneSignal` object from `react-native-onesignal`:
| SendUniqueOutcome(name) | `OneSignal.Session.addUniqueOutcome(name)` |
| SendOutcomeWithValue(name, value) | `OneSignal.Session.addOutcomeWithValue(name, value)` |
| TrackEvent(name, properties) | `OneSignal.User.trackEvent(name, properties)` |
| SetLanguage(language) | `OneSignal.User.setLanguage(language)` |
| GetPushSubscriptionId() | `await OneSignal.User.pushSubscription.getIdAsync()` |
| GetPushSubscriptionToken() | `await OneSignal.User.pushSubscription.getTokenAsync()` |
| IsPushOptedIn() | `await OneSignal.User.pushSubscription.getOptedInAsync()` |
| OptInPush() | `OneSignal.User.pushSubscription.optIn()` |
| OptOutPush() | `OneSignal.User.pushSubscription.optOut()` |
Expand Down Expand Up @@ -232,6 +234,7 @@ OneSignal.User.addEventListener('change', handler);
### Persistence

- `PreferencesService` wraps `AsyncStorage`
- Selected language is persisted for the User section dropdown only; the native SDK keeps its own language, so it is not re-applied on launch. Logout resets it to Device Default (`''`).
- Triggers list (`triggersList`) is NOT persisted

### SDK State Restoration
Expand Down
89 changes: 89 additions & 0 deletions examples/demo/src/components/modals/SingleSelectModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import { Modal, View, Text, TouchableOpacity, ScrollView, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';

import { AppColors, AppTextStyles, AppDialogStyles } from '../../theme';

export interface SelectOption {
label: string;
value: string;
}

interface Props {
visible: boolean;
title: string;
options: SelectOption[];
selectedValue: string;
onSelect: (value: string) => void;
onClose: () => void;
testIDPrefix?: string;
}

export default function SingleSelectModal({
visible,
title,
options,
selectedValue,
onSelect,
onClose,
testIDPrefix,
}: Props) {
const handleSelect = (value: string) => {
onSelect(value);
onClose();
};

return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<View style={AppDialogStyles.backdrop}>
<View style={[AppDialogStyles.container, styles.containerMaxHeight]}>
<Text style={AppDialogStyles.title}>{title}</Text>
<ScrollView style={styles.scroll}>
{options.map(({ label, value }) => {
const isSelected = value === selectedValue;
return (
<TouchableOpacity
key={value}
style={styles.row}
onPress={() => handleSelect(value)}
testID={testIDPrefix ? `${testIDPrefix}_${value || 'default'}` : undefined}
>
<Icon
name={isSelected ? 'radio-button-checked' : 'radio-button-unchecked'}
size={22}
color={isSelected ? AppColors.osPrimary : AppColors.osGrey600}
/>
<Text style={styles.itemLabel}>{label}</Text>
</TouchableOpacity>
);
})}
</ScrollView>
<View style={AppDialogStyles.actions}>
<TouchableOpacity style={AppDialogStyles.actionBtn} onPress={onClose}>
<Text style={AppDialogStyles.actionText}>Cancel</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
}

const styles = StyleSheet.create({
containerMaxHeight: {
maxHeight: '70%',
},
scroll: {
maxHeight: 400,
},
row: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
gap: 12,
},
itemLabel: {
...AppTextStyles.bodyLarge,
color: '#212121',
},
});
17 changes: 16 additions & 1 deletion examples/demo/src/components/sections/PushSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ToggleRow from '../ToggleRow';

interface Props {
pushSubscriptionId: string | undefined;
pushSubscriptionToken: string | undefined;
isPushEnabled: boolean;
hasNotificationPermission: boolean;
onSetPushEnabled: (value: boolean) => void;
Expand All @@ -17,12 +18,15 @@ interface Props {

export default function PushSection({
pushSubscriptionId,
pushSubscriptionToken,
isPushEnabled,
hasNotificationPermission,
onSetPushEnabled,
onPromptPush,
onInfoTap,
}: Props) {
const isSubscribed = isPushEnabled && hasNotificationPermission && !!pushSubscriptionToken;

return (
<SectionCard title="Push" onInfoTap={onInfoTap} sectionKey="push">
<View style={AppTheme.card}>
Expand All @@ -38,8 +42,15 @@ export default function PushSection({
</Text>
</View>
<View style={styles.divider} />
<View style={styles.idRow}>
<Text style={styles.idLabel}>Status</Text>
<Text style={styles.statusValue} testID="push_subscription_status">
{isSubscribed ? 'Subscribed' : 'Unsubscribed'}
</Text>
</View>
<View style={styles.divider} />
<ToggleRow
label="Enabled"
label="Opted In"
value={isPushEnabled}
onValueChange={onSetPushEnabled}
disabled={!hasNotificationPermission}
Expand Down Expand Up @@ -74,6 +85,10 @@ const styles = StyleSheet.create({
marginLeft: 8,
fontFamily: 'monospace',
},
statusValue: {
...AppTextStyles.bodySmall,
color: AppColors.osGrey700,
},
divider: {
height: 1,
backgroundColor: AppColors.osDivider,
Expand Down
95 changes: 93 additions & 2 deletions examples/demo/src/components/sections/UserSection.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,80 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';

import { AppColors, AppTextStyles, AppTheme, AppSpacing } from '../../theme';
import ActionButton from '../ActionButton';
import SingleInputModal from '../modals/SingleInputModal';
import SingleSelectModal, { type SelectOption } from '../modals/SingleSelectModal';
import SectionCard from '../SectionCard';

const LANGUAGE_OPTIONS: SelectOption[] = [
{ label: 'Device Default', value: '' },
{ label: 'English', value: 'en' },
{ label: 'Arabic', value: 'ar' },
{ label: 'Azerbaijani', value: 'az' },
{ label: 'Bosnian', value: 'bs' },
{ label: 'Catalan', value: 'ca' },
{ label: 'Chinese (Simplified)', value: 'zh-Hans' },
{ label: 'Chinese (Traditional)', value: 'zh-Hant' },
{ label: 'Croatian', value: 'hr' },
{ label: 'Czech', value: 'cs' },
{ label: 'Danish', value: 'da' },
{ label: 'Dutch', value: 'nl' },
{ label: 'Estonian', value: 'et' },
{ label: 'Finnish', value: 'fi' },
{ label: 'French', value: 'fr' },
{ label: 'Georgian', value: 'ka' },
{ label: 'Bulgarian', value: 'bg' },
{ label: 'German', value: 'de' },
{ label: 'Greek', value: 'el' },
{ label: 'Hindi', value: 'hi' },
{ label: 'Hebrew', value: 'he' },
{ label: 'Hungarian', value: 'hu' },
{ label: 'Indonesian', value: 'id' },
{ label: 'Italian', value: 'it' },
{ label: 'Japanese', value: 'ja' },
{ label: 'Korean', value: 'ko' },
{ label: 'Latvian', value: 'lv' },
{ label: 'Lithuanian', value: 'lt' },
{ label: 'Malay', value: 'ms' },
{ label: 'Norwegian', value: 'nb' },
{ label: 'Persian', value: 'fa' },
{ label: 'Polish', value: 'pl' },
{ label: 'Portuguese', value: 'pt' },
{ label: 'Punjabi', value: 'pa' },
{ label: 'Romanian', value: 'ro' },
{ label: 'Russian', value: 'ru' },
{ label: 'Serbian', value: 'sr' },
{ label: 'Slovak', value: 'sk' },
{ label: 'Spanish', value: 'es' },
{ label: 'Swedish', value: 'sv' },
{ label: 'Thai', value: 'th' },
{ label: 'Turkish', value: 'tr' },
{ label: 'Ukrainian', value: 'uk' },
{ label: 'Vietnamese', value: 'vi' },
];

interface Props {
externalUserId: string | undefined;
language: string;
onLogin: (externalUserId: string) => Promise<void>;
onLogout: () => Promise<void>;
onSetLanguage: (language: string) => Promise<void>;
}

export default function UserSection({ externalUserId, onLogin, onLogout }: Props) {
export default function UserSection({
externalUserId,
language,
onLogin,
onLogout,
onSetLanguage,
}: Props) {
const [loginOpen, setLoginOpen] = useState(false);
const [languageOpen, setLanguageOpen] = useState(false);
const isLoggedIn = !!externalUserId;
const languageLabel =
LANGUAGE_OPTIONS.find((option) => option.value === language)?.label ?? language;

const handleLogin = async (userId: string) => {
await onLogin(userId);
Expand Down Expand Up @@ -43,6 +103,20 @@ export default function UserSection({ externalUserId, onLogin, onLogout }: Props
{externalUserId ?? '—'}
</Text>
</View>
<View style={styles.divider} />
<TouchableOpacity
style={styles.row}
onPress={() => setLanguageOpen(true)}
testID="user_language_dropdown"
>
<Text style={styles.label}>Language</Text>
<View style={styles.dropdownValue}>
<Text style={styles.languageValue} testID="user_language_value">
{languageLabel}
</Text>
<Icon name="arrow-drop-down" size={22} color={AppColors.osGrey600} />
</View>
</TouchableOpacity>
</View>
<ActionButton
label={isLoggedIn ? 'SWITCH USER' : 'LOGIN USER'}
Expand All @@ -66,6 +140,15 @@ export default function UserSection({ externalUserId, onLogin, onLogout }: Props
onClose={() => setLoginOpen(false)}
testID="login_user_id_input"
/>
<SingleSelectModal
visible={languageOpen}
title="Language"
options={LANGUAGE_OPTIONS}
selectedValue={language}
onSelect={onSetLanguage}
onClose={() => setLanguageOpen(false)}
testIDPrefix="language_option"
/>
</SectionCard>
);
}
Expand All @@ -89,6 +172,14 @@ const styles = StyleSheet.create({
color: AppColors.osGrey700,
fontFamily: 'monospace',
},
dropdownValue: {
flexDirection: 'row',
alignItems: 'center',
},
languageValue: {
...AppTextStyles.bodySmall,
color: AppColors.osGrey700,
},
loggedInText: {
color: AppColors.osSuccess,
},
Expand Down
Loading
Loading