Verified Commit a468f25d authored by Gijs Hendriksen's avatar Gijs Hendriksen Committed by Sébastiaan Versteeg
Browse files

Added settings screen for push notification categories

parent fe2d8c7d
......@@ -8,6 +8,17 @@ Object {
exports[`push notifications actions should create an action to register the push token 1`] = `
Object {
"categories": null,
"type": "PUSH_NOTIFICATIONS_REGISTER",
}
`;
exports[`push notifications actions should create an action to register the push tokens and update push categories 1`] = `
Object {
"categories": Array [
"category1",
"category2",
],
"type": "PUSH_NOTIFICATIONS_REGISTER",
}
`;
......@@ -10,6 +10,10 @@ describe('push notifications actions', () => {
expect(actions.register()).toMatchSnapshot();
});
it('should create an action to register the push tokens and update push categories', () => {
expect(actions.register(['category1', 'category2'])).toMatchSnapshot();
});
it('should create an action to invalidate the push token', () => {
expect(actions.invalidate()).toMatchSnapshot();
});
......
......@@ -403,6 +403,109 @@ exports[`Sidebar component renders correctly 1`] = `
},
]
}
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"justifyContent": "flex-start",
"padding": 8,
},
Object {
"backgroundColor": "#FFFFFF",
"borderRadius": 0,
},
Array [
Object {
"padding": 16,
},
Object {},
],
]
}
>
<Text
accessible={true}
allowFontScaling={false}
ellipsizeMode="tail"
onPress={[Function]}
style={
Array [
Object {
"color": "#313131",
"fontSize": 24,
},
Array [
Object {
"marginRight": 10,
},
Object {
"marginRight": 30,
"textAlign": "center",
"width": 28,
},
],
Object {
"fontFamily": "Material Icons",
"fontStyle": "normal",
"fontWeight": "normal",
},
]
}
>
</Text>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Array [
Object {
"backgroundColor": "transparent",
"fontWeight": "600",
},
Object {
"color": "#313131",
},
]
}
>
Settings
</Text>
</View>
</View>
<View
accessibilityComponentType={undefined}
accessibilityLabel={undefined}
accessibilityTraits={undefined}
accessible={true}
hasTVPreferredFocus={undefined}
hitSlop={undefined}
isTVSelectable={true}
nativeID={undefined}
onLayout={undefined}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"overflow": "hidden",
},
Object {
"backgroundColor": "#FFFFFF",
"borderRadius": 0,
},
]
}
testID={undefined}
tvParallaxProperties={undefined}
>
<View
style={
......
export const REGISTER = 'PUSH_NOTIFICATIONS_REGISTER';
export const INVALIDATE = 'PUSH_NOTIFICATIONS_INVALIDATE';
export function register() {
return { type: REGISTER };
export function register(categories = null) {
return { type: REGISTER, categories };
}
export function invalidate() {
......
// Group setting sections together to prepare for more submenus to be added.
export const pushNotificationsSettingsActions = {
RETRIEVE: 'SETTINGS_PUSH_NOTIFICATIONS',
LOADING: 'SETTINGS_PUSH_NOTIFICATIONS_LOADING',
SUCCESS: 'SETTINGS_PUSH_NOTIFICATIONS_SUCCESS',
FAILURE: 'SETTINGS_PUSH_NOTIFICATIONS_FAILURE',
SAVE_CATEGORIES: 'SETTINGS_PUSH_NOTIFICATIONS_SAVE_CATEGORIES',
retrieve: () => ({
type: pushNotificationsSettingsActions.RETRIEVE,
}),
loading: () => ({
type: pushNotificationsSettingsActions.LOADING,
}),
success: categoryList => ({
type: pushNotificationsSettingsActions.SUCCESS,
categoryList,
}),
failure: () => ({
type: pushNotificationsSettingsActions.FAILURE,
}),
saveCategories: categories => ({
type: pushNotificationsSettingsActions.SAVE_CATEGORIES,
categories,
}),
};
......@@ -26,6 +26,7 @@ const USERNAMEKEY = '@MyStore:username';
const TOKENKEY = '@MyStore:token';
const DISPLAYNAMEKEY = '@MyStore:displayName';
const PHOTOKEY = '@MyStore:photo';
const PUSHCATEGORYKEY = '@MyStore:pushCategories';
const pairsToObject = (obj, pair) => {
const obj2 = { ...obj };
......@@ -62,7 +63,7 @@ class Main extends Component {
}
componentDidMount() {
AsyncStorage.multiGet([USERNAMEKEY, TOKENKEY, DISPLAYNAMEKEY, PHOTOKEY])
AsyncStorage.multiGet([USERNAMEKEY, TOKENKEY, DISPLAYNAMEKEY, PHOTOKEY, PUSHCATEGORYKEY])
.then(
(result) => {
const values = result.reduce(pairsToObject, {});
......@@ -70,12 +71,13 @@ class Main extends Component {
const token = values[TOKENKEY];
const displayName = values[DISPLAYNAMEKEY];
const photo = values[PHOTOKEY];
const pushCategories = JSON.parse(values[PUSHCATEGORYKEY]);
if (username !== null && token !== null) {
store.dispatch(loginActions.success(username, token));
store.dispatch(loginActions.profileSuccess(displayName, photo));
store.dispatch(loginActions.profile(token));
store.dispatch(register());
store.dispatch(register(pushCategories));
}
},
);
......
const files = {};
files['app/ui/screens/user/ProfileNL'] = require('./nl/app/ui/screens/user/Profile.json');
files['app/ui/screens/user/LoginNL'] = require('./nl/app/ui/screens/user/Login.json');
files['app/ui/screens/welcome/WelcomeNL'] = require('./nl/app/ui/screens/welcome/Welcome.json');
files['app/ui/screens/welcome/EventDetailCardNL'] = require('./nl/app/ui/screens/welcome/EventDetailCard.json');
files['app/ui/screens/memberList/MemberListNL'] = require('./nl/app/ui/screens/memberList/MemberList.json');
files['app/ui/screens/welcome/EventDetailCardNL'] = require('./nl/app/ui/screens/welcome/EventDetailCard.json');
files['app/ui/screens/welcome/WelcomeNL'] = require('./nl/app/ui/screens/welcome/Welcome.json');
files['app/ui/screens/pizza/PizzaNL'] = require('./nl/app/ui/screens/pizza/Pizza.json');
files['app/ui/screens/settings/SettingsNL'] = require('./nl/app/ui/screens/settings/Settings.json');
files['app/ui/screens/settings/PushNotificationsNL'] = require('./nl/app/ui/screens/settings/PushNotifications.json');
files['app/ui/screens/events/CalendarNL'] = require('./nl/app/ui/screens/events/Calendar.json');
files['app/ui/screens/events/RegistrationNL'] = require('./nl/app/ui/screens/events/Registration.json');
files['app/ui/screens/events/CalendarItemNL'] = require('./nl/app/ui/screens/events/CalendarItem.json');
files['app/ui/screens/events/EventNL'] = require('./nl/app/ui/screens/events/Event.json');
files['app/ui/screens/events/RegistrationNL'] = require('./nl/app/ui/screens/events/Registration.json');
files['app/ui/screens/events/CalendarNL'] = require('./nl/app/ui/screens/events/Calendar.json');
files['app/ui/screens/pizza/PizzaNL'] = require('./nl/app/ui/screens/pizza/Pizza.json');
files['app/ui/components/standardHeader/StandardHeaderNL'] = require('./nl/app/ui/components/standardHeader/StandardHeader.json');
files['app/ui/components/navigator/SidebarNL'] = require('./nl/app/ui/components/navigator/Sidebar.json');
files['app/ui/components/errorScreen/ErrorScreenNL'] = require('./nl/app/ui/components/errorScreen/ErrorScreen.json');
files['app/ui/components/navigator/SidebarNL'] = require('./nl/app/ui/components/navigator/Sidebar.json');
export default {
nl: {
'screens/user/Profile': files['app/ui/screens/user/ProfileNL'],
'screens/user/Login': files['app/ui/screens/user/LoginNL'],
'screens/welcome/Welcome': files['app/ui/screens/welcome/WelcomeNL'],
'screens/welcome/EventDetailCard': files['app/ui/screens/welcome/EventDetailCardNL'],
'screens/memberList/MemberList': files['app/ui/screens/memberList/MemberListNL'],
'screens/welcome/EventDetailCard': files['app/ui/screens/welcome/EventDetailCardNL'],
'screens/welcome/Welcome': files['app/ui/screens/welcome/WelcomeNL'],
'screens/pizza/Pizza': files['app/ui/screens/pizza/PizzaNL'],
'screens/settings/Settings': files['app/ui/screens/settings/SettingsNL'],
'screens/settings/PushNotifications': files['app/ui/screens/settings/PushNotificationsNL'],
'screens/events/Calendar': files['app/ui/screens/events/CalendarNL'],
'screens/events/Registration': files['app/ui/screens/events/RegistrationNL'],
'screens/events/CalendarItem': files['app/ui/screens/events/CalendarItemNL'],
'screens/events/Event': files['app/ui/screens/events/EventNL'],
'screens/events/Registration': files['app/ui/screens/events/RegistrationNL'],
'screens/events/Calendar': files['app/ui/screens/events/CalendarNL'],
'screens/pizza/Pizza': files['app/ui/screens/pizza/PizzaNL'],
'components/standardHeader/StandardHeader': files['app/ui/components/standardHeader/StandardHeaderNL'],
'components/navigator/Sidebar': files['app/ui/components/navigator/SidebarNL'],
'components/errorScreen/ErrorScreen': files['app/ui/components/errorScreen/ErrorScreenNL'],
'components/navigator/Sidebar': files['app/ui/components/navigator/SidebarNL'],
},
};
......@@ -6,5 +6,6 @@
"Welcome": "Welkom",
"Calendar": "Agenda",
"Logout": "Uitloggen",
"Member List": "Ledenlijst"
"Member List": "Ledenlijst",
"Settings": "Instellingen"
}
......@@ -4,5 +4,7 @@
"Calendar": "Agenda",
"Pizza": "Pizza",
"Profile": "Profiel",
"Registration": "Registratie"
"Registration": "Registratie",
"Settings": "Instellingen",
"Notifications": "Meldingen"
}
{
"Sorry, we couldn't load any data.": "Sorry, we konden geen gegevens laden.",
"(required)": "(verplicht)"
}
......@@ -8,6 +8,7 @@ import profile from './profile';
import pizza from './pizza';
import registration from './registration';
import members from './members';
import settings from './settings';
export default combineReducers({
session,
......@@ -19,4 +20,5 @@ export default combineReducers({
pizza,
registration,
members,
settings,
});
import { pushNotificationsSettingsActions } from '../actions/settings';
const initialState = {
pushNotifications: {
categoryList: [],
status: 'loading',
},
};
export default function settings(state = initialState, action = {}) {
switch (action.type) {
case pushNotificationsSettingsActions.LOADING: {
return {
...state,
pushNotifications: {
...state.pushNotifications,
status: 'loading',
},
};
}
case pushNotificationsSettingsActions.SUCCESS: {
return {
...state,
pushNotifications: {
...state.pushNotifications,
categoryList: action.categoryList,
status: 'success',
},
};
}
case pushNotificationsSettingsActions.FAILURE: {
return {
...state,
pushNotifications: {
...state.pushNotifications,
status: 'failure',
},
};
}
default: {
return state;
}
}
}
......@@ -10,6 +10,7 @@ import pizzaSaga from './pizza';
import registrationSaga from './registration';
import deepLinkingSaga from './deepLinking';
import membersSaga from './members';
import settingsSaga from './settings';
const sagas = function* sagas() {
yield all([
......@@ -23,6 +24,7 @@ const sagas = function* sagas() {
fork(registrationSaga),
fork(deepLinkingSaga),
fork(membersSaga),
fork(settingsSaga),
]);
};
......
......@@ -6,8 +6,9 @@ import { Sentry } from 'react-native-sentry';
import { apiRequest, tokenSelector } from '../utils/url';
import * as pushNotificationsActions from '../actions/pushNotifications';
const register = function* register() {
const register = function* register(action) {
const token = yield select(tokenSelector);
const { categories } = action;
if (token === undefined) {
// There is no token, thus do nothing
......@@ -28,6 +29,15 @@ const register = function* register() {
pushToken = yield call(FCM.getFCMToken);
}
const body = {
registration_id: pushToken,
type: Platform.OS,
};
if (categories !== null) {
body.receive_category = categories;
}
const data = {
method: 'POST',
headers: {
......@@ -35,10 +45,7 @@ const register = function* register() {
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
body: JSON.stringify({
registration_id: pushToken,
type: Platform.OS,
}),
body: JSON.stringify(body),
};
try {
......
import { AsyncStorage } from 'react-native';
import { takeEvery, select, call, put } from 'redux-saga/effects';
import { pushNotificationsSettingsActions } from '../actions/settings';
import * as navigationActions from '../actions/navigation';
import { apiRequest, tokenSelector } from '../utils/url';
const PUSHCATEGORYKEY = '@MyStore:pushCategories';
function* pushNotifications() {
const token = yield select(tokenSelector);
const data = {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
};
yield put(pushNotificationsSettingsActions.loading());
yield put(navigationActions.navigate('pushNotificationsSettings'));
try {
const categoryList = yield call(apiRequest, 'devices/categories', data);
const preferencesJson = yield call(AsyncStorage.getItem, PUSHCATEGORYKEY);
if (preferencesJson === null) {
for (let i = 0; i < categoryList.length; i += 1) {
categoryList[i].enabled = true;
}
} else {
const categoryPreferences = JSON.parse(preferencesJson);
for (let i = 0; i < categoryList.length; i += 1) {
categoryList[i].enabled = categoryPreferences.includes(categoryList[i].key);
}
}
yield put(pushNotificationsSettingsActions.success(categoryList));
} catch (error) {
yield put(pushNotificationsSettingsActions.failure());
}
}
function* saveCategories(action) {
const { categories } = action;
try {
yield call(AsyncStorage.setItem, PUSHCATEGORYKEY, JSON.stringify(categories));
} catch (error) {
// Swallow error
}
}
function* settingsSaga() {
yield takeEvery(pushNotificationsSettingsActions.RETRIEVE, pushNotifications);
yield takeEvery(pushNotificationsSettingsActions.SAVE_CATEGORIES, saveCategories);
}
export default settingsSaga;
......@@ -15,6 +15,8 @@ import Pizza from '../../screens/pizza/Pizza';
import StandardHeader from '../standardHeader/StandardHeader';
import Registration from '../../screens/events/Registration';
import MemberList from '../../screens/memberList/MemberList';
import Settings from '../../screens/settings/Settings';
import PushNotifications from '../../screens/settings/PushNotifications';
import * as actions from '../../../actions/navigation';
import styles from './style/ReduxNavigator';
......@@ -36,6 +38,10 @@ const sceneToComponent = (scene) => {
return <Registration />;
case 'members':
return <MemberList />;
case 'settings':
return <Settings />;
case 'pushNotificationsSettings':
return <PushNotifications />;
default:
return <Welcome />;
}
......
......@@ -47,6 +47,13 @@ const Sidebar = (props) => {
style: {},
scene: 'members',
},
{
onPress: () => props.navigate('settings', true),
iconName: 'settings',
text: props.t('Settings'),
style: {},
scene: 'settings',
},
{
onPress: logoutPrompt(props),
iconName: 'lock',
......
......@@ -26,6 +26,10 @@ const sceneToTitle = (scene, t) => {
return t('Profile');
case 'registration':
return t('Registration');
case 'settings':
return t('Settings');
case 'pushNotificationsSettings':
return t('Notifications');
default:
return 'ThaliApp';
}
......
import React, { Component } from 'react';
import { View, Text, Switch } from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
import ErrorScreen from '../../components/errorScreen/ErrorScreen';
import styles from './style/PushNotifications';
import Colors from '../../style/Colors';
import { pushNotificationsSettingsActions } from '../../../actions/settings';
import * as pushNotificationsActions from '../../../actions/pushNotifications';
class PushNotifications extends Component {
static getDerivedStateFromProps = (props) => {
if (props.status !== 'success') {
return null;
}
const newState = {};
for (let i = 0; i < props.categoryList.length; i += 1) {
newState[props.categoryList[i].key] = props.categoryList[i].enabled;
}
return newState;
};
constructor(props) {
super(props);
this.state = {};
}
updateField = (key, value) => {
const update = {};
update[key] = value;
this.setState(update, () => {
const categories = Object.keys(this.state).filter(k => this.state[k]);
this.props.register(categories);
this.props.saveCategories(categories);
});
};
render() {
if (this.props.status === 'loading') {
return <LoadingScreen />;
} else if (this.props.status === 'failure') {
return <ErrorScreen message={this.props.t('Sorry, we couldn\'t load any data.')} />;
}
return (
<View style={styles.container}>
{this.props.categoryList.map(category => (
<View style={styles.setting} key={category.key}>
<Text
style={styles.label}
key={category.key}
>{category.name} {category.key === 'general' && this.props.t('(required)')}</Text>
<Switch
value={this.state[category.key]}
onValueChange={value => this.updateField(category.key, value)}
thumbTintColor={this.state[category.key] ? Colors.darkMagenta : Colors.lightGray}
onTintColor={Colors.magenta}
disabled={category.key === 'general'}
/>
</View>
))}
</View>
);
}
}
PushNotifications.propTypes = {
categoryList: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired,
})).isRequired,
status: PropTypes.string.isRequired,