Commit 2ad3873d authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Merge branch '58-adjust-profile-picture-in-the-application' into 'master'

Resolve "Adjust profile picture in the application"

Closes #76 and #58

See merge request !272
parents 896b05f8 f2d05d7f
......@@ -8,9 +8,7 @@ Object {
exports[`profile actions should create an action for a successful profile load 1`] = `
Object {
"payload": Object {
"profileData": "profile",
},
"payload": "profile",
"type": "PROFILE_SUCCESS",
}
`;
......
......@@ -49,6 +49,7 @@ Object {
"payload": Object {
"displayName": "displayName",
"photo": "photo",
"pk": "pk",
},
"type": "SESSION_SET_USER_INFO",
}
......
......@@ -36,6 +36,6 @@ describe('session actions', () => {
});
it('should create an action to set user info', () => {
expect(actions.setUserInfo('displayName', 'photo')).toMatchSnapshot();
expect(actions.setUserInfo('pk', 'displayName', 'photo')).toMatchSnapshot();
});
});
\ No newline at end of file
});
......@@ -14,7 +14,6 @@ Object {
"birthday": "",
"display_name": "",
"membership_type": "",
"photo": "http://localhost:8000/static/members/images/default-avatar.jpg",
"pk": -1,
"profile_description": "",
"programme": "",
......
......@@ -4,6 +4,7 @@ exports[`session reducer initially should return the initial state 1`] = `
Object {
"displayName": "",
"photo": "http://localhost:8000/static/members/images/default-avatar.jpg",
"pk": -1,
"status": "SIGNED_OUT",
"token": "",
"username": "",
......
......@@ -54,9 +54,13 @@ describe('session reducer', () => {
describe('has retrieved user info', () => {
const state = reducer(
emptyState,
actions.setUserInfo('John Doe', 'imageUrl'),
actions.setUserInfo('pk', 'John Doe', 'imageUrl'),
);
it('should contain the primary key', () => {
expect(state).toHaveProperty('pk', 'pk');
});
it('should contain the display name', () => {
expect(state).toHaveProperty('displayName', 'John Doe');
});
......
......@@ -142,19 +142,11 @@ describe('members saga', () => {
it('should load more members', () => expectSaga(membersSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.like({ fn: apiRequest, args: ['moreUrl'] }), { results: [{ pk: 1 }], next: 'moreUrl2' }],
])
.dispatch(memberActions.more('moreUrl'))
.silentRun()
.then(() => {
expect(fetch).toBeCalledWith('moreUrl', {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Token token',
},
});
}));
.put(memberActions.moreSuccess([{ pk: 1 }], 'moreUrl2'))
.silentRun());
it('should put the result data when the request succeeds', () => {
const response = {
......
......@@ -7,6 +7,12 @@ import { apiRequest } from '../../app/utils/url';
import * as profileActions from '../../app/actions/profile';
import { tokenSelector } from '../../app/selectors/session';
jest.mock('react-native-snackbar', () => ({
LENGTH_LONG: 100,
show: jest.fn(),
dismiss: jest.fn(),
}));
jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}),
}));
......@@ -57,4 +63,4 @@ describe('profile saga', () => {
method: 'GET',
});
}));
});
\ No newline at end of file
});
......@@ -6,7 +6,7 @@ import AsyncStorage from '@react-native-community/async-storage';
import { Sentry } from 'react-native-sentry';
import sessionSaga, {
DISPLAYNAMEKEY, PHOTOKEY, TOKENKEY, USERNAMEKEY,
DISPLAYNAMEKEY, IDENTIFIERKEY, PHOTOKEY, TOKENKEY, USERNAMEKEY,
} from '../../app/sagas/session';
import { apiRequest } from '../../app/utils/url';
import * as sessionActions from '../../app/actions/session';
......@@ -119,7 +119,7 @@ describe('session saga', () => {
.dispatch(sessionActions.signOut())
.silentRun()
.then(() => {
expect(AsyncStorage.clear).toBeCalled();
expect(AsyncStorage.multiRemove).toBeCalled();
}));
it('should put a push notification invalidation action', () => expectSaga(sessionSaga)
......@@ -141,19 +141,21 @@ describe('session saga', () => {
it('should put the result data when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['members/me'] }), {
pk: 12,
display_name: 'Johnny Test',
avatar: {
medium: 'http://example.org/photo.png',
},
}],
])
.put(sessionActions.setUserInfo('Johnny Test', 'http://example.org/photo.png'))
.put(sessionActions.setUserInfo(12, 'Johnny Test', 'http://example.org/photo.png'))
.dispatch(sessionActions.fetchUserInfo())
.silentRun());
it('should save the token in the AsyncStorage when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['members/me'] }), {
pk: 12,
display_name: 'Johnny Test',
avatar: {
medium: 'http://example.org/photo.png',
......@@ -164,6 +166,7 @@ describe('session saga', () => {
.silentRun()
.then(() => {
expect(AsyncStorage.multiSet).toBeCalledWith([
[IDENTIFIERKEY, 12],
[DISPLAYNAMEKEY, 'Johnny Test'],
[PHOTOKEY, 'http://example.org/photo.png'],
]);
......
import DeviceInfo from 'react-native-device-info';
import {
apiRequest,
apiUrl,
......@@ -7,20 +8,17 @@ import {
TokenInvalidError,
} from '../../app/utils/url';
import DeviceInfo from 'react-native-device-info';
const fetchPromiseResult = {
status: 200,
json: () => Promise.resolve('responseJson'),
clone: global.fetch,
json: () => Promise.resolve({ exampleJson: 'val' }),
};
global.fetch = jest.fn().mockReturnValue(
Promise.resolve(fetchPromiseResult),
);
fetchPromiseResult.clone = global.fetch;
describe('url helper', () => {
beforeEach(() => {
global.fetch = jest.fn().mockReturnValue(
Promise.resolve(fetchPromiseResult),
);
});
it('should expose the constants', () => {
......@@ -37,7 +35,7 @@ describe('url helper', () => {
.then((response) => {
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/`,
{ headers: { 'Accept-Language': 'en' } });
expect(response).toEqual('responseJson');
expect(response).toEqual({ exampleJson: 'val' });
});
});
......@@ -65,7 +63,7 @@ describe('url helper', () => {
.then((response) => {
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/`,
{ headers: { 'Accept-Language': 'en' } });
expect(response).toEqual('responseJson');
expect(response).toEqual({ exampleJson: 'val' });
});
});
......@@ -74,7 +72,6 @@ describe('url helper', () => {
const response = {
status: 404,
json: () => Promise.resolve('responseJson'),
clone: () => ({ status: 404 }),
};
global.fetch.mockReturnValue(Promise.resolve(response));
return apiRequest('route', {}, null)
......@@ -86,7 +83,6 @@ describe('url helper', () => {
const response = {
status: 204,
json: () => Promise.resolve('responseJson'),
clone: () => ({ status: 204 }),
};
global.fetch.mockReturnValue(Promise.resolve(response));
return apiRequest('route', {}, null)
......@@ -99,7 +95,6 @@ describe('url helper', () => {
status: 403,
headers: { get: key => (key === 'content-language' ? 'en' : 'nl') },
json: () => Promise.resolve({ detail: 'Invalid token.' }),
clone: () => 'responseCopy',
};
global.fetch.mockReturnValue(Promise.resolve(response));
return apiRequest('route', {}, null)
......@@ -112,7 +107,6 @@ describe('url helper', () => {
status: 403,
headers: { get: key => (key === 'content-language' ? 'nl' : 'en') },
json: () => Promise.resolve({ detail: 'Ongeldige token.' }),
clone: () => 'responseCopy',
};
global.fetch.mockReturnValue(Promise.resolve(response));
return apiRequest('route', {}, null)
......@@ -125,16 +119,14 @@ describe('url helper', () => {
status: 403,
headers: { get: key => (key === 'content-language' ? 'en' : 'nl') },
json: () => Promise.resolve({ detail: 'Not authorized.' }),
clone: () => ({ json: () => 'jsonResult' }),
};
global.fetch.mockReturnValue(Promise.resolve(response));
return apiRequest('route', {}, null)
.then(res => expect(res).toEqual('jsonResult'));
.catch(res => expect(res).toEqual(new ServerError('Invalid status code: 403')));
});
it('should default to an English locales', () => {
DeviceInfo.getDeviceLocale = () => 'fr';
expect.assertions(1);
return apiRequest('route', {}, null)
.then(() => {
expect(global.fetch).toBeCalledWith(`${apiUrl}/route/`,
......
......@@ -79,6 +79,7 @@ dependencies {
implementation project(':@react-native-community_status-bar')
implementation project(':@react-native-community_async-storage')
implementation project(':react-native-device-info')
implementation project(':react-native-image-picker')
implementation project(':react-native-screens')
implementation project(':react-native-gesture-handler')
implementation project(':react-native-sentry')
......
......@@ -4,10 +4,11 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission tools:node="remove" android:name="android.permission.READ_PHONE_STATE" />
<uses-permission tools:node="remove" android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".MainApplication"
......
......@@ -8,6 +8,7 @@ import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.swmansion.rnscreens.RNScreensPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.imagepicker.ImagePickerPackage;
import cl.json.ShareApplication;
import cl.json.RNSharePackage;
......@@ -44,6 +45,7 @@ public class MainApplication extends Application implements ShareApplication, Re
new RNDeviceInfo(),
new RNScreensPackage(),
new RNGestureHandlerPackage(),
new ImagePickerPackage(),
new RNSharePackage(),
new RNSentryPackage(),
new SnackbarPackage(),
......
......@@ -9,6 +9,8 @@ include ':react-native-screens'
project(':react-native-screens').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-screens/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-share'
project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android')
include ':react-native-sentry'
......
......@@ -2,6 +2,9 @@ export const PROFILE = 'PROFILE_PROFILE';
export const FETCHING = 'PROFILE_FETCHING';
export const SUCCESS = 'PROFILE_SUCCESS';
export const FAILURE = 'PROFILE_FAILURE';
export const UPDATE = 'PROFILE_UPDATE';
export const UPDATE_SUCCESS = 'PROFILE_UPDATE_SUCCESS';
export const CHANGE_AVATAR = 'PROFILE_CHANGE_AVATAR';
export function profile(member = 'me') {
return {
......@@ -16,10 +19,23 @@ export function fetching() {
};
}
export function changeAvatar() {
return {
type: CHANGE_AVATAR,
};
}
export function updateSuccess(profileData) {
return {
type: UPDATE_SUCCESS,
payload: profileData,
};
}
export function success(profileData) {
return {
type: SUCCESS,
payload: { profileData },
payload: profileData,
};
}
......
......@@ -30,6 +30,6 @@ export function fetchUserInfo() {
return { type: FETCH_USER_INFO };
}
export function setUserInfo(displayName, photo) {
return { type: SET_USER_INFO, payload: { displayName, photo } };
export function setUserInfo(pk, displayName, photo) {
return { type: SET_USER_INFO, payload: { pk, displayName, photo } };
}
......@@ -6,7 +6,6 @@ const initialState = {
profile: {
pk: -1,
display_name: '',
photo: defaultProfileImage,
avatar: {
full: defaultProfileImage,
large: defaultProfileImage,
......@@ -26,8 +25,8 @@ const initialState = {
hasLoaded: false,
};
export default function profile(state = initialState, action = {}) {
switch (action.type) {
export default function profile(state = initialState, { type, payload } = {}) {
switch (type) {
case profileActions.FETCHING:
return {
...state,
......@@ -36,7 +35,7 @@ export default function profile(state = initialState, action = {}) {
case profileActions.SUCCESS:
return {
...state,
profile: action.payload.profileData,
profile: payload,
success: true,
hasLoaded: true,
};
......@@ -46,6 +45,14 @@ export default function profile(state = initialState, action = {}) {
success: false,
hasLoaded: true,
};
case profileActions.UPDATE_SUCCESS:
return {
...state,
profile: {
...state.profile,
...payload,
},
};
default:
return state;
}
......
......@@ -11,6 +11,7 @@ const initialState = {
token: '',
username: '',
displayName: '',
pk: -1,
photo: defaultProfileImage,
};
......@@ -31,6 +32,7 @@ export default function session(state = initialState, action = {}) {
case sessionActions.SET_USER_INFO:
return {
...state,
pk: action.payload.pk,
displayName: action.payload.displayName,
photo: action.payload.photo,
};
......
import {
call, put, select, takeEvery,
} from 'redux-saga/effects';
import { Sentry } from 'react-native-sentry';
import { apiRequest } from '../utils/url';
import * as calendarActions from '../actions/calendar';
import { tokenSelector } from '../selectors/session';
import reportError from '../utils/errorReporting';
const calendar = function* calendar() {
const token = yield select(tokenSelector);
......@@ -31,17 +31,15 @@ const calendar = function* calendar() {
partner: true,
}));
} catch (error) {
Sentry.captureException(error);
yield call(reportError, error);
}
yield put(calendarActions.success(events.concat(partnerEvents)));
} catch (error) {
Sentry.captureException(error);
yield call(reportError, error);
yield put(calendarActions.failure());
}
};
const calendarSaga = function* eventSaga() {
export default function* () {
yield takeEvery(calendarActions.REFRESH, calendar);
};
export default calendarSaga;
}
......@@ -81,8 +81,6 @@ const deepLink = function* deepLink(action) {
}
};
const deepLinkingSaga = function* deepLinkingSaga() {
export default function* () {
yield takeEvery(deepLinkingActions.DEEPLINK, deepLink);
};
export default deepLinkingSaga;
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment