Verified Commit f2d05d7f authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Finish profile picture update

parent c51c51a2
......@@ -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
});
......@@ -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)
......
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/`,
......
......@@ -3,9 +3,7 @@ export const FETCHING = 'PROFILE_FETCHING';
export const SUCCESS = 'PROFILE_SUCCESS';
export const FAILURE = 'PROFILE_FAILURE';
export const UPDATE = 'PROFILE_UPDATE';
export const UPDATING = 'PROFILE_UPDATING';
export const UPDATE_SUCCESS = 'PROFILE_UPDATE_SUCCESS';
export const UPDATE_FAIL = 'PROFILE_UPDATE_FAIL';
export const CHANGE_AVATAR = 'PROFILE_CHANGE_AVATAR';
export function profile(member = 'me') {
......@@ -27,35 +25,17 @@ export function changeAvatar() {
};
}
export function update() {
return {
type: UPDATE,
};
}
export function updating() {
return {
type: UPDATING,
};
}
export function updateSuccess(profileData) {
return {
type: UPDATE_SUCCESS,
payload: { profileData },
};
}
export function updateFail() {
return {
type: UPDATE_FAIL,
payload: profileData,
};
}
export function success(profileData) {
return {
type: SUCCESS,
payload: { profileData },
payload: profileData,
};
}
......
......@@ -23,11 +23,10 @@ const initialState = {
},
success: false,
hasLoaded: false,
updating: 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,16 +45,13 @@ export default function profile(state = initialState, action = {}) {
success: false,
hasLoaded: true,
};
case profileActions.UPDATING:
return {
...state,
updating: true,
};
case profileActions.UPDATE_FAIL:
case profileActions.UPDATE_SUCCESS:
return {
...state,
updating: false,
profile: {
...state.profile,
...payload,
},
};
default:
return state;
......
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;
}
import {
call, put, select, takeEvery,
} from 'redux-saga/effects';
import { Sentry } from 'react-native-sentry';
import { apiRequest } from '../utils/url';
import * as eventActions from '../actions/event';
import { tokenSelector } from '../selectors/session';
import reportError from '../utils/errorReporting';
function* event(action) {
const { pk, navigateToEventScreen } = action.payload;
......@@ -40,7 +40,7 @@ function* event(action) {
eventRegistrations,
));
} catch (error) {
Sentry.captureException(error);
yield call(reportError, error);
yield put(eventActions.failure());
}
}
......@@ -69,14 +69,12 @@ function* updateRegistration(action) {
yield put(eventActions.done());
} catch (error) {
Sentry.captureException(error);
yield call(reportError, error);
yield put(eventActions.failure());
}
}
function* eventSaga() {
export default function* () {
yield takeEvery(eventActions.EVENT, event);
yield takeEvery(eventActions.UPDATE_REGISTRATION, updateRegistration);
}
export default eventSaga;
......@@ -13,7 +13,7 @@ import deepLinkingSaga from './deepLinking';
import membersSaga from './members';
import settingsSaga from './settings';
const sagas = function* sagas() {
export default function* () {
yield all([
fork(sessionSaga),
fork(navigationSaga),
......@@ -28,6 +28,4 @@ const sagas = function* sagas() {
fork(membersSaga),
fork(settingsSaga),
]);
};
export default sagas;
}
......@@ -2,7 +2,6 @@ import { Dimensions } from 'react-native';
import {
call, put, select, takeEvery,
} from 'redux-saga/effects';
import { Sentry } from 'react-native-sentry';
import { TOTAL_BAR_HEIGHT } from '../ui/components/standardHeader/style/StandardHeader';
import { memberSize } from '../ui/screens/memberList/style/MemberList';
......@@ -10,6 +9,7 @@ import { memberSize } from '../ui/screens/memberList/style/MemberList';
import { apiRequest } from '../utils/url';
import * as memberActions from '../actions/members';
import { tokenSelector } from '../selectors/session';
import reportError from '../utils/errorReporting';
const members = function* members(action) {
const { keywords } = action.payload;
......@@ -40,7 +40,7 @@ const members = function* members(action) {
const response = yield call(apiRequest, 'members', data, params);
yield put(memberActions.success(response.results, response.next, keywords));
} catch (error) {
Sentry.captureException(error);
yield call(reportError, error);
yield put(memberActions.failure());
}
};
......@@ -61,17 +61,15 @@ const more = function* more(action) {
};
try {
const responseJson = yield fetch(url, data).then(response => response.json());
yield put(memberActions.moreSuccess(responseJson.results, responseJson.next));
const response = yield call(apiRequest, url, data);
yield put(memberActions.moreSuccess(response.results, response.next));
} catch (error) {
Sentry.captureException(error);
yield call(reportError, error);
yield put(memberActions.moreSuccess([], null));
}
};
const membersSaga = function* membersSaga() {
export default function* () {
yield takeEvery(memberActions.MEMBERS, members);
yield takeEvery(memberActions.MORE, more);
};
export default membersSaga;
}
......@@ -28,7 +28,7 @@ function openWebsite({ payload: url }) {
Linking.openURL(url);
}
const routerSaga = function* eventSaga() {
export default function* () {
yield takeEvery(navigationActions.BACK, back);
yield takeEvery(navigationActions.TOGGLE_DRAWER, toggleDrawer);
yield takeEvery(navigationActions.OPEN_WEBSITE, openWebsite);
......@@ -44,6 +44,4 @@ const routerSaga = function* eventSaga() {
yield takeEvery(pizzaActions.PIZZA, navigate, 'Pizza');
yield takeEvery(sessionActions.SIGNED_IN, navigate, 'SignedIn');
yield takeEvery([sessionActions.TOKEN_INVALID, sessionActions.SIGN_OUT], navigate, 'Auth');
};
export default routerSaga;
}
import {
call, put, select, takeEvery,
} from 'redux-saga/effects';
import { Sentry } from 'react-native-sentry';
import { apiRequest } from '../utils/url';
import * as pizzaActions from '../actions/pizza';
import { tokenSelector } from '../selectors/session';
import reportError from '../utils/errorReporting';
export const Payment = {
NONE: 'no_payment',
......@@ -39,7 +39,7 @@ const retrievePizzaInfo = function* retrievePizzaInfo() {
if (error.response !== null && error.response.status === NOT_FOUND) {
yield put(pizzaActions.success(event, null, pizzaList));
} else {
Sentry.captureException(error);
yield call(reportError, error);
yield put(pizzaActions.failure());
}
}
......@@ -47,7 +47,7 @@ const retrievePizzaInfo = function* retrievePizzaInfo() {
if (error.response !== null && error.response.status === NOT_FOUND) {
yield put(pizzaActions.success(null, null, []));
} else {
Sentry.captureException(error);
yield call(reportError, error);
yield put(pizzaActions.failure());
}
}
......@@ -68,7 +68,7 @@ const cancel = function* cancel() {
yield call(apiRequest, 'pizzas/orders/me', data);
yield put(pizzaActions.cancelSuccess());
} catch (error) {
Sentry.captureException(error);
yield call(reportError, error);
yield put(pizzaActions.failure());
}
};
......@@ -93,15 +93,13 @@ const order = function* order(action) {
const orderData = yield call(apiRequest, route, data);
yield put(pizzaActions.orderSuccess(orderData));
} catch (error) {
Sentry.captureException(error);
yield call(reportError, error);
yield put(pizzaActions.failure());
}
};
const pizzaSaga = function* pizzaSaga() {
export default function* () {
yield takeEvery(pizzaActions.PIZZA, retrievePizzaInfo);
yield takeEvery(pizzaActions.CANCEL, cancel);
yield takeEvery(pizzaActions.ORDER, order);
};
export default pizzaSaga;
}
import ImagePicker from 'react-native-image-picker';
import {
call, put, select, takeEvery, cps
call, put, select, takeEvery,
} from 'redux-saga/effects';
import { Sentry } from 'react-native-sentry';
import { Alert, Platform } from 'react-native';
import Snackbar from 'react-native-snackbar';
import i18next from '../utils/i18n';
import { apiRequest } from '../utils/url';
import * as profileActions from '../actions/profile';
import { tokenSelector } from '../selectors/session';
import reportError from '../utils/errorReporting';
const t = i18next.getFixedT(undefined, 'sagas/profile');
const openImageLibrary = options => new Promise((resolve, reject) => {
ImagePicker.launchImageLibrary(options, (response) => {
ImagePicker.showImagePicker(options, (response) => {
if (response.error || response.didCancel) {
reject(response);
}
......@@ -39,7 +41,7 @@ function* profile(action) {
const profileData = yield call(apiRequest, `members/${member}`, data);
yield put(profileActions.success(profileData));
} catch (error) {
Sentry.captureException(error);
yield call(reportError, error);
yield put(profileActions.failure());
}
}
......@@ -54,39 +56,50 @@ function* updateAvatar() {
};
try {
const response = yield call(openImageLibrary, options);
const source = response.uri;
console.log(source);
const photo = yield call(openImageLibrary, options);
const formData = new FormData();
formData.append('photo', {
name: photo.fileName,
type: photo.type,
uri:
Platform.OS === 'android' ? photo.uri : photo.uri.replace('file://', ''),
});
const token = yield select(tokenSelector);
const data = {
method: 'PATCH',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
body: formData,
};
try {
yield call([Snackbar, 'show'], { title: t('Uploading your new profile picture...'), duration: Snackbar.LENGTH_INDEFINITE });
const profileData = yield call(apiRequest, 'members/me', data);
yield call([Snackbar, 'dismiss']);
yield put(profileActions.success(profileData));
} catch (error) {
yield call([Snackbar, 'dismiss']);
yield call(reportError, error);
if ('photo' in error.response.jsonData) {
yield call(Alert.alert, t('Could not update profile picture'), error.response.jsonData.photo.join(' '));
} else {
yield call([Snackbar, 'show'], { title: t('Could not update profile picture') });
}
}
} catch (e) {
// eat error, om nom nom
// error from the picker that we cannot do anything about
}
// const { member } = action.payload;
// const token = yield select(tokenSelector);