) => {
34 | const store = createStore({ ...mockReduxStoreState, ...state });
35 | const dispatch = jest.fn();
36 | store.dispatch = dispatch;
37 |
38 | const wrapper = render(
39 |
40 |
41 | ,
42 | );
43 |
44 | return {
45 | wrapper,
46 | dispatch,
47 | };
48 | };
49 |
50 | describe('QuestionDetails', () => {
51 | it('should render question data based on a given id form route parameters', async () => {
52 | const { wrapper } = setup();
53 |
54 | const questionTitle = await wrapper.queryByText(
55 | mockQuestionObject.question,
56 | );
57 |
58 | const choiceTitle = await wrapper.queryByText(mockChoiceObject.choice);
59 |
60 | expect(questionTitle).not.toBeNull();
61 | expect(choiceTitle).not.toBeNull();
62 | });
63 |
64 | it('should dispatch choicesActions.postVoteRequested when tapping on ChoiceListItem if not voted yet', async () => {
65 | const { wrapper, dispatch } = setup();
66 |
67 | const choiceItem = await wrapper.getByText(mockChoiceObject.choice);
68 | act(() => fireEvent(choiceItem, 'press'));
69 |
70 | expect(dispatch).toHaveBeenCalledWith(
71 | choicesActions.postVoteRequested({
72 | question_id: mockQuestionId,
73 | choice_id: mockChoiceId,
74 | }),
75 | );
76 | });
77 |
78 | it('should NOT dispatch choicesActions.postVoteRequested when tapping on ChoiceListItem if already voted', async () => {
79 | const { wrapper, dispatch } = setup({
80 | ...mockReduxStoreState,
81 | choices: {
82 | ...mockReduxStoreState.choices,
83 | votedChoiceByQuestionId: { [mockQuestionId]: mockChoiceId },
84 | },
85 | });
86 |
87 | const choiceItem = await wrapper.getByText(mockChoiceObject.choice);
88 | await waitFor(() => choiceItem);
89 | act(() => fireEvent(choiceItem, 'press'));
90 |
91 | expect(dispatch).not.toHaveBeenCalled();
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/src/screens/Questions/Questions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | FlatList,
4 | StyleSheet,
5 | Animated,
6 | ActivityIndicator,
7 | View,
8 | Platform,
9 | } from 'react-native';
10 |
11 | import { NavBar } from '../../navigation/NavBar';
12 | import { styleValues, colors } from '../../styles';
13 | import { Screen, PrimaryButton } from '../../components';
14 |
15 | import { useQuestions } from './hooks/useQuestions';
16 | import { QuestionListItem } from './components/QuestionListItem';
17 |
18 | export const questionsRouteName = 'Questions';
19 | export const CREATE_NEW_BUTTON_TEST_ID = 'CREATE_NEW_BUTTON_TEST_ID';
20 | export const ACTIVITY_INDICATOR_TEST_ID = 'ACTIVITY_INDICATOR_TEST_ID';
21 | export const QUESTIONS_LIST_ID = 'QUESTIONS_LIST_ID';
22 |
23 | export const Questions = () => {
24 | const {
25 | ids,
26 | status,
27 | opacity,
28 | onEndReached,
29 | onPressCreate,
30 | onPressQuestion,
31 | } = useQuestions();
32 |
33 | const renderItem = ({ item }: { item: number }) => (
34 |
35 | );
36 |
37 | const renderListFooter = () =>
38 | status === 'loading' ? (
39 |
44 | ) : null;
45 |
46 | return (
47 |
48 |
49 |
50 |
65 |
66 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | const keyExtractor = (id: number) => String(id);
79 |
80 | const styles = StyleSheet.create({
81 | animatedContainer: {
82 | flex: 1,
83 | overflow: 'hidden',
84 | backgroundColor: colors.transparent,
85 | },
86 | flatList: {
87 | overflow: 'scroll',
88 | },
89 | flatListContainer: {
90 | flexGrow: 1,
91 | paddingHorizontal: styleValues.spacings.medium,
92 | paddingBottom: styleValues.spacings.extraLarge,
93 | },
94 | navbarTitleStyle: {
95 | color: colors.white,
96 | },
97 | primaryButtonContainer: {
98 | position: 'absolute',
99 | bottom: styleValues.spacings.medium,
100 | left: 0,
101 | right: 0,
102 | alignItems: 'center',
103 | justifyContent: 'center',
104 | },
105 | });
106 |
--------------------------------------------------------------------------------
/src/store/slices/__tests__/choicesSlice.test.ts:
--------------------------------------------------------------------------------
1 | import { choicesReducer, choicesActions, ChoicesState } from '../choicesSlice';
2 | import {
3 | mockChoiceObject,
4 | mockQuestionAndChoiceObjects,
5 | mockChoiceId,
6 | mockQuestionId,
7 | } from '../../../test/mocks';
8 | import { questionsActions } from '../questionsSlice';
9 |
10 | const mockEmptyState: ChoicesState = {
11 | byId: {},
12 | votedChoiceByQuestionId: {},
13 | };
14 |
15 | describe('choicesSlice', () => {
16 | describe('choicesReducer', () => {
17 | it('should handle choicesActions.postVoteRequested action', () => {
18 | const mockState = {
19 | ...mockEmptyState,
20 | byId: {
21 | [mockChoiceId]: mockChoiceObject,
22 | },
23 | };
24 | const newState = choicesReducer(
25 | mockState,
26 | choicesActions.postVoteRequested({
27 | question_id: mockQuestionId,
28 | choice_id: mockChoiceId,
29 | }),
30 | );
31 |
32 | expect(newState).toEqual({
33 | votedChoiceByQuestionId: {
34 | [mockQuestionId]: mockChoiceId,
35 | },
36 | byId: {
37 | [mockChoiceId]: {
38 | ...mockChoiceObject,
39 | votes: mockChoiceObject.votes + 1,
40 | },
41 | },
42 | });
43 | });
44 |
45 | it('should handle choicesActions.postVoteSucceeded action', () => {
46 | const mockState = {
47 | ...mockEmptyState,
48 | byId: {
49 | [mockChoiceId]: mockChoiceObject,
50 | },
51 | };
52 | const mockVotes = 25;
53 | const mockNewChoiceObject = {
54 | ...mockChoiceObject,
55 | votes: mockVotes,
56 | };
57 | const newState = choicesReducer(
58 | mockState,
59 | choicesActions.postVoteSucceeded({ choice: mockNewChoiceObject }),
60 | );
61 |
62 | expect(newState).toEqual({
63 | ...mockState,
64 | byId: {
65 | [mockChoiceId]: mockNewChoiceObject,
66 | },
67 | });
68 | });
69 |
70 | it('should handle questionsActions.getQuestionsAndChoicesSucceeded action', () => {
71 | const newState = choicesReducer(
72 | mockEmptyState,
73 | questionsActions.getQuestionsAndChoicesSucceeded({
74 | questions: mockQuestionAndChoiceObjects.questionObjects,
75 | choices: mockQuestionAndChoiceObjects.choiceObjects,
76 | }),
77 | );
78 |
79 | expect(newState).toEqual({
80 | ...mockEmptyState,
81 | byId: {
82 | [mockQuestionAndChoiceObjects.choiceObjects[0].id]:
83 | mockQuestionAndChoiceObjects.choiceObjects[0],
84 | },
85 | });
86 | });
87 |
88 | it('should handle postQuestionSucceeded action', () => {
89 | const newState = choicesReducer(
90 | mockEmptyState,
91 | questionsActions.postQuestionSucceeded({
92 | questions: mockQuestionAndChoiceObjects.questionObjects,
93 | choices: mockQuestionAndChoiceObjects.choiceObjects,
94 | }),
95 | );
96 |
97 | expect(newState).toEqual({
98 | ...mockEmptyState,
99 | byId: { [mockChoiceObject.id]: mockChoiceObject },
100 | });
101 | });
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/src/services/APIHelpers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | QuestionObjectResponse,
3 | QuestionObject,
4 | ChoiceObject,
5 | ChoiceObjectResponse,
6 | } from '../types/questions';
7 |
8 | const onlyNumbersRegex = /\d+/g;
9 |
10 | const getLastIdFromUrl = (url: string) => {
11 | const match = url.match(onlyNumbersRegex);
12 |
13 | return match && match?.length > 0 ? Number(match[match.length - 1]) : -1;
14 | };
15 |
16 | const getFirstIdFromUrl = (url: string) => {
17 | const match = url.match(onlyNumbersRegex);
18 |
19 | return match && match?.length > 0 ? Number(match[0]) : -1;
20 | };
21 |
22 | type QuestionAndChoiceObjects = {
23 | questionObjects: QuestionObject[];
24 | choiceObjects: ChoiceObject[];
25 | };
26 | const convertQuestionsResponseToQuestionAndChoiceObjects = (
27 | questions: QuestionObjectResponse[],
28 | ) =>
29 | questions.reduce(
30 | (acc, questionResponse) => {
31 | const questionId = getLastIdFromUrl(questionResponse.url);
32 | const questionObject: QuestionObject = {
33 | id: questionId,
34 | question: questionResponse.question,
35 | published_at: new Date(questionResponse.published_at),
36 | choice_ids: questionResponse.choices.map((choice) => {
37 | const choice_id = getLastIdFromUrl(choice.url);
38 | const choiceObject: ChoiceObject = {
39 | id: choice_id,
40 | questionId,
41 | votes: choice.votes,
42 | choice: choice.choice,
43 | };
44 |
45 | acc.choiceObjects.push(choiceObject);
46 |
47 | return choice_id;
48 | }),
49 | };
50 |
51 | acc.questionObjects.push(questionObject);
52 |
53 | return acc;
54 | },
55 | {
56 | questionObjects: [],
57 | choiceObjects: [],
58 | } as QuestionAndChoiceObjects,
59 | );
60 |
61 | const convertChoiceResponseToChoiceObject = ({
62 | choiceResponse,
63 | }: {
64 | choiceResponse: ChoiceObjectResponse;
65 | }) => {
66 | const choice_id = APIHelpers.getLastIdFromUrl(choiceResponse.url); // "/questions/1/choices/2" -> gets 2
67 | const choiceObject: ChoiceObject = {
68 | id: choice_id,
69 | questionId: APIHelpers.getFirstIdFromUrl(choiceResponse.url), // "/questions/1/choices/2" -> gets 1
70 | votes: choiceResponse.votes,
71 | choice: choiceResponse.choice,
72 | };
73 |
74 | return choiceObject;
75 | };
76 |
77 | const parseLinks = (response: Response) => {
78 | const linksString = response.headers.get('link');
79 | const linksData = linksString?.split('link:');
80 | const data = linksData?.length == 2 ? linksData[1] : linksString;
81 | const parsedData: { [key in string]: string } = {};
82 |
83 | const linkAndRelNames = data?.split(',');
84 |
85 | if (linkAndRelNames === undefined) {
86 | return undefined;
87 | }
88 |
89 | for (const item of linkAndRelNames) {
90 | const linkInfo = /<([^>]+)>;\s+rel="([^"]+)"/gi.exec(item);
91 |
92 | const relName = linkInfo && linkInfo[2];
93 | const link = linkInfo && linkInfo[1];
94 | if (relName && link) {
95 | parsedData[relName] = link;
96 | }
97 | }
98 |
99 | return parsedData;
100 | };
101 |
102 | const getGenericErrorMessage = () => 'Unexpected error occurred';
103 |
104 | export const APIHelpers = {
105 | getLastIdFromUrl,
106 | getFirstIdFromUrl,
107 | getGenericErrorMessage,
108 | convertChoiceResponseToChoiceObject,
109 | convertQuestionsResponseToQuestionAndChoiceObjects,
110 | parseLinks,
111 | };
112 |
--------------------------------------------------------------------------------
/android/app/src/debug/java/com/heycarchallenge/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.pollsapp;
8 |
9 | import android.content.Context;
10 | import com.facebook.flipper.android.AndroidFlipperClient;
11 | import com.facebook.flipper.android.utils.FlipperUtils;
12 | import com.facebook.flipper.core.FlipperClient;
13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
22 | import com.facebook.react.ReactInstanceManager;
23 | import com.facebook.react.bridge.ReactContext;
24 | import com.facebook.react.modules.network.NetworkingModule;
25 | import okhttp3.OkHttpClient;
26 |
27 | public class ReactNativeFlipper {
28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
29 | if (FlipperUtils.shouldEnableFlipper(context)) {
30 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
31 |
32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
33 | client.addPlugin(new ReactFlipperPlugin());
34 | client.addPlugin(new DatabasesFlipperPlugin(context));
35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
36 | client.addPlugin(CrashReporterPlugin.getInstance());
37 |
38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
39 | NetworkingModule.setCustomClientBuilder(
40 | new NetworkingModule.CustomClientBuilder() {
41 | @Override
42 | public void apply(OkHttpClient.Builder builder) {
43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
44 | }
45 | });
46 | client.addPlugin(networkFlipperPlugin);
47 | client.start();
48 |
49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
50 | // Hence we run if after all native modules have been initialized
51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
52 | if (reactContext == null) {
53 | reactInstanceManager.addReactInstanceEventListener(
54 | new ReactInstanceManager.ReactInstanceEventListener() {
55 | @Override
56 | public void onReactContextInitialized(ReactContext reactContext) {
57 | reactInstanceManager.removeReactInstanceEventListener(this);
58 | reactContext.runOnNativeModulesQueueThread(
59 | new Runnable() {
60 | @Override
61 | public void run() {
62 | client.addPlugin(new FrescoFlipperPlugin());
63 | }
64 | });
65 | }
66 | });
67 | } else {
68 | client.addPlugin(new FrescoFlipperPlugin());
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/screens/QuestionDetails/components/ChoiceListItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect, useRef } from 'react';
2 | import {
3 | View,
4 | Image,
5 | Animated,
6 | StyleSheet,
7 | TouchableOpacityProps,
8 | } from 'react-native';
9 | import { TouchableOpacity } from 'react-native-gesture-handler';
10 |
11 | import { images } from '../../../assets';
12 | import { colors, styleValues } from '../../../styles';
13 | import { BodySmall, DelayedRender } from '../../../components';
14 |
15 | interface Props extends TouchableOpacityProps {
16 | title: string;
17 | percentage: number;
18 | isChosen?: boolean;
19 | hasBeenVoted: boolean;
20 | }
21 |
22 | export const ChoiceListItem: FC = ({
23 | title,
24 | percentage,
25 | onPress,
26 | isChosen,
27 | disabled,
28 | hasBeenVoted,
29 | }) => {
30 | const { width } = useProgressBarAnimation({
31 | percentage,
32 | hasBeenVoted,
33 | });
34 | const borderWidth = hasBeenVoted ? 0 : 1;
35 |
36 | return (
37 |
38 |
39 |
42 | {title}
43 | {hasBeenVoted && (
44 |
45 | {isChosen && (
46 |
47 | )}
48 |
49 | {getPercentageString(percentage)}
50 |
51 |
52 | )}
53 |
54 |
55 | );
56 | };
57 |
58 | const useProgressBarAnimation = ({
59 | percentage,
60 | hasBeenVoted,
61 | }: Pick) => {
62 | const initialPercentage = hasBeenVoted ? percentage : 0;
63 | const progress = useRef(new Animated.Value(initialPercentage)).current;
64 | useEffect(() => {
65 | Animated.timing(progress, {
66 | duration: 300,
67 | toValue: percentage,
68 | useNativeDriver: false, // nativeDriver does not support animating width unfortunately
69 | }).start();
70 | }, [percentage]);
71 |
72 | const width = progress.interpolate({
73 | inputRange: [0, 100],
74 | outputRange: ['0%', '100%'],
75 | });
76 |
77 | return { width };
78 | };
79 |
80 | const getPercentageString = (percentage: number) =>
81 | ' ' + Math.round(percentage) + '%';
82 |
83 | const styles = StyleSheet.create({
84 | percentageBar: {
85 | backgroundColor: colors.secondary,
86 | },
87 | container: {
88 | borderColor: colors.secondary,
89 | borderWidth: 1,
90 | opacity: 1,
91 | flex: 1,
92 | flexDirection: 'row',
93 | alignItems: 'center',
94 | overflow: 'hidden',
95 | justifyContent: 'space-between',
96 | paddingVertical: styleValues.spacings.medium,
97 | marginTop: styleValues.spacings.small,
98 | borderRadius: styleValues.borderRadius,
99 | },
100 | icon: {
101 | marginRight: styleValues.spacings.small,
102 | tintColor: colors.black,
103 | ...styleValues.iconSizes.large,
104 | },
105 | title: {
106 | flexWrap: 'wrap',
107 | paddingHorizontal: 20,
108 | flexShrink: 1,
109 | },
110 | percentage: {
111 | fontWeight: 'bold',
112 | },
113 | percentageContainer: {
114 | flexDirection: 'row',
115 | alignItems: 'center',
116 | marginRight: styleValues.spacings.medium,
117 | },
118 | });
119 |
--------------------------------------------------------------------------------
/ios/PollsApp/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/screens/QuestionDetails/QuestionDetails.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import { useDispatch } from 'react-redux';
4 | import { ScrollView } from 'react-native-gesture-handler';
5 | import { useNavigation } from '@react-navigation/native';
6 |
7 | import {
8 | Card,
9 | BodyRegular,
10 | DelayedRender,
11 | TouchableIcon,
12 | } from '../../components';
13 | import { NavBar } from '../../navigation/NavBar';
14 | import { styleValues, colors } from '../../styles';
15 | import { choicesActions } from '../../store/slices/choicesSlice';
16 |
17 | import { ChoiceListItem } from './components/ChoiceListItem';
18 | import { useQuestionDetails } from './hooks/useQuestionDetails';
19 |
20 | export const questionDetailsRouteName = 'QuestionDetails';
21 |
22 | export const QuestionDetails = () => {
23 | const {
24 | question,
25 | sumOfVotes,
26 | votedChoice,
27 | choicesById,
28 | hasBeenVoted,
29 | } = useQuestionDetails();
30 | const dispatch = useDispatch();
31 | const navigation = useNavigation();
32 |
33 | const onPressChoice = (args: {
34 | question_id: number;
35 | choice_id: number;
36 | }) => () => {
37 | // Added the check for testing purposes. Since TouchableOpacity mock doesn't support functionality of disabling (https://github.com/callstack/react-native-testing-library/issues/156#issuecomment-483125409)
38 | if (!hasBeenVoted) {
39 | dispatch(choicesActions.postVoteRequested(args));
40 | }
41 | };
42 |
43 | const renderLeftItem = () => (
44 | {
48 | navigation.goBack();
49 | }}
50 | />
51 | );
52 |
53 | return (
54 |
55 |
61 |
65 |
66 | {question.question}
67 |
68 |
69 | {question.choice_ids.map((id) => {
70 | const choice = choicesById[id];
71 | const isChosen = hasBeenVoted && votedChoice === id;
72 | const percentage = hasBeenVoted
73 | ? countPercentage(choice.votes, sumOfVotes)
74 | : 0;
75 |
76 | return (
77 |
89 | );
90 | })}
91 |
92 |
93 |
94 | );
95 | };
96 |
97 | const countPercentage = (value: number, sum: number) => {
98 | if (!sum || !value) {
99 | return 0;
100 | }
101 | return (value / sum) * 100;
102 | };
103 |
104 | const styles = StyleSheet.create({
105 | scrollView: {
106 | flexGrow: 1,
107 | },
108 | card: {
109 | marginHorizontal: styleValues.spacings.medium,
110 | marginVertical: styleValues.spacings.extraLarge,
111 | },
112 | navbar: {
113 | height: styleValues.spacings.large,
114 | paddingHorizontal: 0,
115 | marginBottom: styleValues.spacings.medium,
116 | },
117 | navbarTitle: {
118 | color: colors.black,
119 | },
120 | questionText: {
121 | fontWeight: '400',
122 | textAlign: 'center',
123 | },
124 | buttonsContainer: {
125 | marginTop: styleValues.spacings.medium,
126 | },
127 | });
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Polls app
2 |
3 | This is an application that lets users vote through the public [Polls API](https://pollsapi.docs.apiary.io/).
4 |
5 | The project uses:
6 |
7 | - [React Native](https://facebook.github.io/react-native/docs/getting-started) for iOS & Android app development
8 | - [TypeScript](https://www.typescriptlang.org/docs/home.html) for type-safety
9 | - [Redux](https://redux.js.org/api/api-reference) for state management
10 | - [Sagas](https://redux-saga.js.org/) for managing side effects
11 | - [React Native Testing Library](https://callstack.github.io/react-native-testing-library/) for testing the components
12 |
13 | The aim of this project is to be used as a reference on how to:
14 |
15 | - test and structure a `React Native` + `Redux` project
16 | - use `TypeScript` and `Redux-Saga` in a `React Native` + `Redux` project
17 |
18 | ## Supported features
19 |
20 | | Pagination | Voting | Creating questions |
21 | | ---------------------------------- | :------------------------: | -----------------------------: |
22 | |  |  |  |
23 |
24 | ## Structure of the project
25 |
26 | The folder structure of the project looks as follows:
27 |
28 | ```
29 | src
30 | └── assets
31 | └── components
32 | └── navigation
33 | └── screens
34 | └── services
35 | ├── store
36 | │ └── sagas
37 | │ ├── selectors
38 | │ └── slices
39 | └── styles
40 | └── test
41 | └── types
42 | └── App.tsx
43 | ```
44 |
45 | ### assets
46 |
47 | Contains images used in the project. Other shared assets should be stored here (e.g. fonts, videos, etc.).
48 |
49 | ### components
50 |
51 | Contains reusable UI components.
52 |
53 | ### navigation
54 |
55 | Contains navigation related files (`Navigators`, `NavBar`, etc.).
56 |
57 | ### screens
58 |
59 | Contains screens of the app. Each screen has dedicated folders for components and hooks.
60 |
61 | ### services
62 |
63 | Contains `APIClient` and `APIHelpers`. Other services should be stored here (e.g. localization, error tracking, etc.).
64 |
65 | ### store
66 |
67 | - sagas: `Sagas` used for side effects of `Redux actions`
68 | - selectors: `Selectors` to retrieve and compute derived data from the `Redux state`
69 | - slices: `Slices` of the `Redux state` which contain `actions` and `reducers`
70 |
71 | ### styles
72 |
73 | Contains colors and other style values (icon sizes, spacings, etc.) used throughout the app.
74 |
75 | ### test
76 |
77 | Contains test related files (jest setup, mocks, etc.).
78 |
79 | ### types
80 |
81 | Contains the types used throughout the project.
82 |
83 | ---
84 |
85 | ### NOTE
86 |
87 | For bigger projects I'd recommend creating separate folders for each feature. That way it's easier to navigate the project. Also, several developers can work on different features without having to worry too much about merge conflicts.
88 |
89 | Example:
90 |
91 | ```
92 | src
93 | └── assets
94 | └── components
95 | └── navigation
96 | └── services
97 | ├── features
98 | │ └── login
99 | │ | └── components
100 | │ | ├── screens
101 | │ | └── store
102 | │ └── questions
103 | │ | └── components
104 | │ | ├── screens
105 | │ | └── store
106 | └── styles
107 | └── test
108 | └── types
109 | └── App.tsx
110 | ```
111 |
112 | Please, keep in mind that there are many other ways in which you can structure a `React Native` project. This is the approach that has worked for me. Feel free to adopt it either partially or fully and see if it fits your needs.
113 |
114 | ---
115 |
116 | ## Running
117 |
118 | Install dependencies:
119 |
120 | ```
121 | yarn
122 |
123 | cd ios && pod install
124 | ```
125 |
126 | Start packager:
127 |
128 | ```
129 | yarn start
130 | ```
131 |
132 | ### iOS:
133 |
134 | Run the app:
135 |
136 | ```
137 | yarn ios
138 | ```
139 |
140 | If you prefer Xcode rather than command line:
141 |
142 | - open `./ios/PollsApp.xcworkspace` in Xcode
143 | - select a simulator or a device
144 | - hit the Run button
145 |
146 | ### Android:
147 |
148 | Have an Android emulator running (quickest way to get started), or a device connected
149 |
150 | Run the app:
151 |
152 | ```
153 | yarn android
154 | ```
155 |
--------------------------------------------------------------------------------
/src/screens/QuestionCreation/QuestionCreation.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, ActivityIndicator } from 'react-native';
3 | import { TextInput, ScrollView } from 'react-native-gesture-handler';
4 |
5 | import {
6 | Card,
7 | bodyStyles,
8 | PrimaryButton,
9 | TouchableIcon,
10 | } from '../../components';
11 | import { NavBar } from '../../navigation/NavBar';
12 | import { styleValues, colors } from '../../styles';
13 |
14 | import {
15 | useQuestionCreation,
16 | questionTitleChanged,
17 | choiceValueChanged,
18 | newChoiceAdded,
19 | } from './hooks/useQuestionCreation';
20 |
21 | export const questionCreationRouteName = 'QuestionCreation';
22 | export const QUESTION_TITLE_TEXT_INPUT_TEST_ID =
23 | 'QUESTION_TITLE_TEXT_INPUT_TEST_ID';
24 | export const CHOICE_TEXT_INPUT_TEST_ID = 'CHOICE_TEXT_INPUT_TEST_ID';
25 | export const QUESTION_SUBMIT_BUTTON_TEST_ID = 'QUESTION_SUBMIT_BUTTON_TEST_ID';
26 | export const ADD_CHOICE_BUTTON_TEST_ID = 'ADD_CHOICE_BUTTON_TEST_ID_';
27 |
28 | export const QuestionCreation = () => {
29 | const {
30 | status,
31 | localState,
32 | localDispatch,
33 | onPressBack,
34 | onPressSubmit,
35 | } = useQuestionCreation();
36 |
37 | const renderLeftItem = () => (
38 |
39 | );
40 |
41 | const renderRightItem = () => {
42 | if (status === 'loading') {
43 | return ;
44 | }
45 |
46 | return (
47 |
54 | );
55 | };
56 |
57 | return (
58 |
59 |
66 |
67 | {
73 | localDispatch(questionTitleChanged({ value: text }));
74 | }}
75 | />
76 | {localState.choicesValues.ids.map((id) => (
77 | {
84 | localDispatch(choiceValueChanged({ id, value: text }));
85 | }}
86 | />
87 | ))}
88 | {
91 | localDispatch(newChoiceAdded());
92 | }}
93 | type="WithIcon"
94 | iconName="plus"
95 | testID={ADD_CHOICE_BUTTON_TEST_ID}
96 | />
97 |
98 |
99 | );
100 | };
101 |
102 | const styles = StyleSheet.create({
103 | card: {
104 | flex: 1,
105 | marginHorizontal: styleValues.spacings.medium,
106 | marginVertical: styleValues.spacings.extraLarge,
107 | },
108 | navbar: {
109 | paddingHorizontal: 0,
110 | height: styleValues.spacings.large,
111 | marginBottom: styleValues.spacings.medium,
112 | },
113 | navbarTitle: {
114 | color: colors.black,
115 | },
116 | scrollView: {
117 | flexGrow: 1,
118 | },
119 | textInput: {
120 | flexWrap: 'wrap',
121 | borderBottomWidth: 1,
122 | color: colors.black,
123 | borderBottomColor: colors.secondary,
124 | fontSize: bodyStyles.regular.fontSize,
125 | paddingVertical: styleValues.spacings.small,
126 | },
127 | choiceTextInput: {
128 | fontSize: bodyStyles.small.fontSize,
129 | marginTop: styleValues.spacings.medium,
130 | },
131 | });
132 |
--------------------------------------------------------------------------------
/src/screens/QuestionCreation/__tests__/QuestionCreation.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, fireEvent, waitFor } from 'react-native-testing-library';
3 | import { act } from 'react-test-renderer';
4 | import { Provider } from 'react-redux';
5 |
6 | import {
7 | mockQuestionId,
8 | mockChoiceObject,
9 | mockQuestionBody,
10 | mockQuestionObject,
11 | mockReduxStoreState,
12 | } from '../../../test/mocks';
13 | import {
14 | QuestionCreation,
15 | ADD_CHOICE_BUTTON_TEST_ID,
16 | CHOICE_TEXT_INPUT_TEST_ID,
17 | QUESTION_SUBMIT_BUTTON_TEST_ID,
18 | QUESTION_TITLE_TEXT_INPUT_TEST_ID,
19 | } from '../QuestionCreation';
20 | import { RootState, createStore } from '../../../store';
21 | import { questionsActions } from '../../../store/slices/questionsSlice';
22 |
23 | jest.mock('@react-navigation/native', () => {
24 | const actualNav = jest.requireActual('@react-navigation/native');
25 | return {
26 | ...actualNav,
27 | useIsFocused: () => jest.fn(),
28 | useNavigation: () => ({
29 | navigation: () => jest.fn(),
30 | }),
31 | useRoute: () => ({
32 | params: {
33 | id: mockQuestionId,
34 | },
35 | }),
36 | };
37 | });
38 |
39 | const setup = (state?: Partial) => {
40 | const store = createStore({ ...mockReduxStoreState, ...state });
41 | const dispatch = jest.fn();
42 | store.dispatch = dispatch;
43 |
44 | const wrapper = render(
45 |
46 |
47 | ,
48 | );
49 |
50 | return {
51 | wrapper,
52 | dispatch,
53 | };
54 | };
55 |
56 | describe('QuestionDetails', () => {
57 | it('should take inputs from TextInputs and dispatch questionsActions.postQuestionRequested', async () => {
58 | const { wrapper, dispatch } = setup();
59 |
60 | // Enter question title
61 | const questionTitleTextInput = await wrapper.getByTestId(
62 | QUESTION_TITLE_TEXT_INPUT_TEST_ID,
63 | );
64 | act(() =>
65 | fireEvent.changeText(questionTitleTextInput, mockQuestionObject.question),
66 | );
67 |
68 | // Enter choice 1
69 | const choiceTextInput1 = await wrapper.getByTestId(
70 | CHOICE_TEXT_INPUT_TEST_ID + '1',
71 | );
72 | act(() => fireEvent.changeText(choiceTextInput1, mockChoiceObject.choice));
73 | await waitFor(() => choiceTextInput1);
74 |
75 | // Enter choice 2
76 | const choiceTextInput2 = await wrapper.getByTestId(
77 | CHOICE_TEXT_INPUT_TEST_ID + '2',
78 | );
79 | act(() => fireEvent.changeText(choiceTextInput2, mockChoiceObject.choice));
80 | await waitFor(() => choiceTextInput2);
81 |
82 | // Submit
83 | const submitButton = await wrapper.getByTestId(
84 | QUESTION_SUBMIT_BUTTON_TEST_ID,
85 | );
86 | act(() => fireEvent(submitButton, 'press'));
87 |
88 | expect(dispatch).toHaveBeenCalledWith(
89 | questionsActions.postQuestionRequested({
90 | questionBody: {
91 | ...mockQuestionBody,
92 | // duplicating choices since we entered the same choice twice above
93 | choices: [...mockQuestionBody.choices, mockQuestionBody.choices[0]],
94 | },
95 | }),
96 | );
97 | });
98 |
99 | it('should NOT dispatch questionsActions.postQuestionRequested if info is missing', async () => {
100 | const { wrapper, dispatch } = setup();
101 |
102 | // Press submit
103 | const submitButton = await wrapper.getByTestId(
104 | QUESTION_SUBMIT_BUTTON_TEST_ID,
105 | );
106 | act(() => fireEvent(submitButton, 'press'));
107 |
108 | // questionsActions.postQuestionRequested not dispatched since we haven't provided any 'choices'
109 | expect(dispatch).not.toHaveBeenCalled();
110 | });
111 |
112 | it('should add new text input for choice input when add more button pressed', async () => {
113 | const { wrapper } = setup();
114 |
115 | expect(wrapper.queryByTestId(CHOICE_TEXT_INPUT_TEST_ID + '3')).toBeNull();
116 |
117 | // Press add more
118 | const addMoreButton = await wrapper.getByTestId(ADD_CHOICE_BUTTON_TEST_ID);
119 | act(() => fireEvent(addMoreButton, 'press'));
120 |
121 | // check if third choice has been created
122 | expect(
123 | wrapper.queryByTestId(CHOICE_TEXT_INPUT_TEST_ID + '3'),
124 | ).not.toBeNull();
125 | });
126 | });
127 |
--------------------------------------------------------------------------------
/src/screens/QuestionCreation/hooks/useQuestionCreation.ts:
--------------------------------------------------------------------------------
1 | import { useReducer, useRef, useEffect } from 'react';
2 | import { Alert } from 'react-native';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { useNavigation } from '@react-navigation/native';
5 | import produce from 'immer';
6 |
7 | import { QuestionBody } from '../../../types';
8 | import { getCreationStatus } from '../../../store/selectors';
9 | import { questionsActions } from '../../../store/slices/questionsSlice';
10 |
11 | export const useQuestionCreation = () => {
12 | const status = useSelector(getCreationStatus);
13 | const previousStatus = useRef(status);
14 | const navigation = useNavigation();
15 | useEffect(() => {
16 | if (previousStatus.current !== status) {
17 | previousStatus.current = status;
18 | if (status === 'success') {
19 | navigation.goBack();
20 | }
21 | }
22 | }, [status, navigation]);
23 |
24 | const dispatch = useDispatch();
25 | const [localState, localDispatch] = useReducer(reducer, initialState);
26 |
27 | const onPressBack = () => {
28 | navigation.goBack();
29 | };
30 |
31 | const onPressSubmit = () => {
32 | const questionBody = convertStateToQuestionBody(localState);
33 | if (isQuestionBodyValid(questionBody)) {
34 | dispatch(questionsActions.postQuestionRequested({ questionBody }));
35 | } else {
36 | Alert.alert(
37 | 'Please, make sure that your question has at least 2 choices',
38 | );
39 | }
40 | };
41 |
42 | return {
43 | status,
44 | localState,
45 | localDispatch,
46 | onPressBack,
47 | onPressSubmit,
48 | };
49 | };
50 |
51 | export const reducer = (state: State, action: Actions) =>
52 | produce(state, (draft) => {
53 | switch (action.type) {
54 | case 'QUESTIONS_TITLE_CHANGED':
55 | draft.questionTitle = action.value;
56 | break;
57 | case 'CHOICE_VALUE_CHANGED':
58 | draft.choicesValues.byId[action.id].value = action.value;
59 | break;
60 | case 'NEW_CHOICE_ADDED': {
61 | const nextId = draft.nextChoiceId;
62 | draft.choicesValues.byId[nextId] = {
63 | id: nextId,
64 | placeHolder: 'Choice ' + nextId,
65 | value: '',
66 | };
67 | draft.choicesValues.ids.push(nextId);
68 | draft.nextChoiceId = nextId + 1;
69 | break;
70 | }
71 | }
72 |
73 | return draft;
74 | });
75 |
76 | const isQuestionBodyValid = (questionBody: QuestionBody) =>
77 | questionBody.question.length > 0 && questionBody.choices.length > 1;
78 |
79 | const convertStateToQuestionBody = (state: State) => {
80 | const choices = state.choicesValues.ids
81 | .filter((id) => state.choicesValues.byId[id].value.length > 0)
82 | .map((id) => state.choicesValues.byId[id].value);
83 | const question = state.questionTitle;
84 |
85 | return {
86 | question,
87 | choices,
88 | };
89 | };
90 |
91 | const QUESTIONS_TITLE_CHANGED = 'QUESTIONS_TITLE_CHANGED';
92 | export const questionTitleChanged = ({ value }: { value: string }) => ({
93 | type: QUESTIONS_TITLE_CHANGED as typeof QUESTIONS_TITLE_CHANGED,
94 | value,
95 | });
96 |
97 | const CHOICE_VALUE_CHANGED = 'CHOICE_VALUE_CHANGED';
98 | export const choiceValueChanged = ({
99 | id,
100 | value,
101 | }: {
102 | id: number;
103 | value: string;
104 | }) => ({
105 | type: CHOICE_VALUE_CHANGED as typeof CHOICE_VALUE_CHANGED,
106 | id,
107 | value,
108 | });
109 |
110 | const NEW_CHOICE_ADDED = 'NEW_CHOICE_ADDED';
111 | export const newChoiceAdded = () => ({
112 | type: NEW_CHOICE_ADDED as typeof NEW_CHOICE_ADDED,
113 | });
114 |
115 | type Actions =
116 | | ReturnType
117 | | ReturnType
118 | | ReturnType;
119 |
120 | type TextInputObject = {
121 | id: number;
122 | placeHolder: string;
123 | value: string;
124 | };
125 | type State = {
126 | questionTitle: string;
127 | choicesValues: {
128 | byId: { [key in number]: TextInputObject };
129 | ids: number[];
130 | };
131 | nextChoiceId: number;
132 | };
133 | const initialState: State = {
134 | questionTitle: '',
135 | choicesValues: {
136 | byId: {
137 | [1]: {
138 | id: 1,
139 | placeHolder: 'Choice 1',
140 | value: '',
141 | },
142 | [2]: {
143 | id: 1,
144 | placeHolder: 'Choice 2',
145 | value: '',
146 | },
147 | },
148 | ids: [1, 2],
149 | },
150 | nextChoiceId: 3,
151 | };
152 |
--------------------------------------------------------------------------------
/src/screens/Questions/__tests__/Questions.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, waitFor, fireEvent } from 'react-native-testing-library';
3 | import { Provider } from 'react-redux';
4 |
5 | import {
6 | mockQuestionObject,
7 | mockReduxStoreState,
8 | mockNextUrl,
9 | } from '../../../test/mocks';
10 | import {
11 | Questions,
12 | QUESTIONS_LIST_ID,
13 | ACTIVITY_INDICATOR_TEST_ID,
14 | } from '../../../screens/Questions/Questions';
15 | import { RootState, createStore } from '../../../store';
16 | import { questionsActions } from '../../../store/slices/questionsSlice';
17 |
18 | jest.mock('@react-navigation/native', () => {
19 | const actualNav = jest.requireActual('@react-navigation/native');
20 | return {
21 | ...actualNav,
22 | useIsFocused: () => jest.fn(),
23 | useNavigation: () => ({
24 | navigation: () => jest.fn(),
25 | }),
26 | };
27 | });
28 |
29 | const setup = (state?: Partial) => {
30 | const store = createStore({ ...mockReduxStoreState, ...state });
31 | const dispatch = jest.fn();
32 | store.dispatch = dispatch;
33 |
34 | const wrapper = render(
35 |
36 |
37 | ,
38 | );
39 |
40 | return {
41 | wrapper,
42 | dispatch,
43 | };
44 | };
45 |
46 | const scrollEventData = {
47 | nativeEvent: {
48 | contentOffset: {
49 | y: 500,
50 | },
51 | contentSize: {
52 | // Dimensions of the scrollable content
53 | height: 500,
54 | width: 100,
55 | },
56 | layoutMeasurement: {
57 | // Dimensions of the device
58 | height: 100,
59 | width: 100,
60 | },
61 | },
62 | };
63 |
64 | describe('Questions', () => {
65 | describe('empty state', () => {
66 | it('should dispatch questionsActions.getQuestionsRequested action and display activity indicator', async () => {
67 | const { wrapper, dispatch } = setup({
68 | questions: {
69 | status: 'loading',
70 | byId: {},
71 | ids: [],
72 | creationStatus: 'idle',
73 | },
74 | });
75 |
76 | // check if activity indicator is displayed
77 | const activityIndicator = await wrapper.queryByTestId(
78 | ACTIVITY_INDICATOR_TEST_ID,
79 | );
80 | await waitFor(() => activityIndicator);
81 |
82 | expect(dispatch).toHaveBeenCalledWith(
83 | questionsActions.getQuestionsRequested(),
84 | );
85 | expect(activityIndicator).not.toBeNull();
86 | });
87 | });
88 |
89 | describe('non empty state', () => {
90 | it('should render questions from the store', async () => {
91 | const { wrapper } = setup();
92 |
93 | // check if question title from the store is displayed
94 | const questionTitle = await wrapper.queryByText(
95 | mockQuestionObject.question,
96 | );
97 | await waitFor(() => questionTitle);
98 |
99 | expect(questionTitle).not.toBeNull();
100 | });
101 | });
102 |
103 | describe('pagination', () => {
104 | describe('when nextUrl is available', () => {
105 | it('should dispatch getQuestionsRequested on end reached', () => {
106 | const { wrapper, dispatch } = setup({
107 | questions: {
108 | ...mockReduxStoreState.questions,
109 | nextLink: mockNextUrl,
110 | status: 'success',
111 | },
112 | });
113 |
114 | fireEvent.scroll(
115 | wrapper.getByTestId(QUESTIONS_LIST_ID),
116 | scrollEventData,
117 | );
118 |
119 | // called once on render and once on scroll end reached
120 | expect(dispatch.mock.calls).toEqual([
121 | [questionsActions.getQuestionsRequested()],
122 | [questionsActions.getQuestionsRequested()],
123 | ]);
124 | });
125 | });
126 |
127 | describe('when nextUrl is NOT available', () => {
128 | it('should NOT dispatch getQuestionsRequested on end reached', () => {
129 | const { wrapper, dispatch } = setup({
130 | questions: {
131 | ...mockReduxStoreState.questions,
132 | nextLink: undefined,
133 | status: 'success',
134 | },
135 | });
136 |
137 | fireEvent.scroll(
138 | wrapper.getByTestId(QUESTIONS_LIST_ID),
139 | scrollEventData,
140 | );
141 |
142 | // called only once on render
143 | expect(dispatch.mock.calls).toEqual([
144 | [questionsActions.getQuestionsRequested()],
145 | ]);
146 | });
147 | });
148 | });
149 | });
150 |
--------------------------------------------------------------------------------
/src/store/slices/__tests__/questionsSlice.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | questionsReducer,
3 | QuestionsState,
4 | questionsActions,
5 | } from '../questionsSlice';
6 | import {
7 | mockQuestionObject,
8 | mockQuestionAndChoiceObjects,
9 | mockQuestionBody,
10 | } from '../../../test/mocks';
11 |
12 | const mockEmptyState: QuestionsState = {
13 | status: 'idle',
14 | ids: [],
15 | byId: {},
16 | creationStatus: 'idle',
17 | };
18 |
19 | describe('questionsSlice', () => {
20 | describe('questionsReducer', () => {
21 | it('should handle getQuestionsRequested action', () => {
22 | const newState = questionsReducer(
23 | mockEmptyState,
24 | questionsActions.getQuestionsRequested(),
25 | );
26 |
27 | expect(newState).toEqual({
28 | ...mockEmptyState,
29 | status: 'loading',
30 | });
31 | });
32 |
33 | it('should handle getQuestionsFailed action', () => {
34 | const mockErrorMessage = 'Unexpected error occurred';
35 | const newState = questionsReducer(
36 | mockEmptyState,
37 | questionsActions.getQuestionsFailed({ errorMessage: mockErrorMessage }),
38 | );
39 |
40 | expect(newState).toEqual({
41 | ...mockEmptyState,
42 | status: 'error',
43 | errorMessage: mockErrorMessage,
44 | });
45 | });
46 |
47 | it('should handle postQuestionRequested action', () => {
48 | const newState = questionsReducer(
49 | mockEmptyState,
50 | questionsActions.postQuestionRequested({
51 | questionBody: mockQuestionBody,
52 | }),
53 | );
54 |
55 | expect(newState).toEqual({
56 | ...mockEmptyState,
57 | creationStatus: 'loading',
58 | });
59 | });
60 |
61 | it('should handle postQuestionFailed action', () => {
62 | const mockErrorMessage = 'Unexpected error occurred';
63 | const newState = questionsReducer(
64 | mockEmptyState,
65 | questionsActions.postQuestionFailed({ errorMessage: mockErrorMessage }),
66 | );
67 |
68 | expect(newState).toEqual({
69 | ...mockEmptyState,
70 | creationStatus: 'error',
71 | creationError: mockErrorMessage,
72 | });
73 | });
74 |
75 | describe('getQuestionsSucceeded', () => {
76 | it('should handle action', () => {
77 | const newState = questionsReducer(
78 | mockEmptyState,
79 | questionsActions.getQuestionsAndChoicesSucceeded({
80 | questions: mockQuestionAndChoiceObjects.questionObjects,
81 | choices: mockQuestionAndChoiceObjects.choiceObjects,
82 | }),
83 | );
84 |
85 | expect(newState).toEqual({
86 | ...mockEmptyState,
87 | status: 'success',
88 | byId: { [mockQuestionObject.id]: mockQuestionObject },
89 | ids: [mockQuestionObject.id],
90 | });
91 | });
92 |
93 | it('should NOT add duplicates', () => {
94 | const mockNonEmptyState: QuestionsState = {
95 | ...mockEmptyState,
96 | status: 'success',
97 | byId: { [mockQuestionObject.id]: mockQuestionObject },
98 | ids: [mockQuestionObject.id],
99 | };
100 | const newState = questionsReducer(
101 | mockNonEmptyState,
102 | questionsActions.getQuestionsAndChoicesSucceeded({
103 | questions: mockQuestionAndChoiceObjects.questionObjects,
104 | choices: mockQuestionAndChoiceObjects.choiceObjects,
105 | }),
106 | );
107 |
108 | expect(newState).toEqual(mockNonEmptyState);
109 | });
110 | });
111 |
112 | describe('postQuestionSucceeded', () => {
113 | it('should handle action', () => {
114 | const newState = questionsReducer(
115 | mockEmptyState,
116 | questionsActions.postQuestionSucceeded({
117 | questions: mockQuestionAndChoiceObjects.questionObjects,
118 | choices: mockQuestionAndChoiceObjects.choiceObjects,
119 | }),
120 | );
121 |
122 | expect(newState).toEqual({
123 | ...mockEmptyState,
124 | creationStatus: 'success',
125 | byId: { [mockQuestionObject.id]: mockQuestionObject },
126 | ids: [mockQuestionObject.id],
127 | });
128 | });
129 |
130 | it('should NOT add duplicates', () => {
131 | const mockNonEmptyState: QuestionsState = {
132 | ...mockEmptyState,
133 | creationStatus: 'success',
134 | byId: { [mockQuestionObject.id]: mockQuestionObject },
135 | ids: [mockQuestionObject.id],
136 | };
137 | const newState = questionsReducer(
138 | mockNonEmptyState,
139 | questionsActions.postQuestionSucceeded({
140 | questions: mockQuestionAndChoiceObjects.questionObjects,
141 | choices: mockQuestionAndChoiceObjects.choiceObjects,
142 | }),
143 | );
144 |
145 | expect(newState).toEqual(mockNonEmptyState);
146 | });
147 | });
148 | });
149 | });
150 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | platform :ios, '9.0'
2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
3 |
4 | def add_flipper_pods!(versions = {})
5 | versions['Flipper'] ||= '~> 0.33.1'
6 | versions['DoubleConversion'] ||= '1.1.7'
7 | versions['Flipper-Folly'] ||= '~> 2.1'
8 | versions['Flipper-Glog'] ||= '0.3.6'
9 | versions['Flipper-PeerTalk'] ||= '~> 0.0.4'
10 | versions['Flipper-RSocket'] ||= '~> 1.0'
11 |
12 | pod 'FlipperKit', versions['Flipper'], :configuration => 'Debug'
13 | pod 'FlipperKit/FlipperKitLayoutPlugin', versions['Flipper'], :configuration => 'Debug'
14 | pod 'FlipperKit/SKIOSNetworkPlugin', versions['Flipper'], :configuration => 'Debug'
15 | pod 'FlipperKit/FlipperKitUserDefaultsPlugin', versions['Flipper'], :configuration => 'Debug'
16 | pod 'FlipperKit/FlipperKitReactPlugin', versions['Flipper'], :configuration => 'Debug'
17 |
18 | # List all transitive dependencies for FlipperKit pods
19 | # to avoid them being linked in Release builds
20 | pod 'Flipper', versions['Flipper'], :configuration => 'Debug'
21 | pod 'Flipper-DoubleConversion', versions['DoubleConversion'], :configuration => 'Debug'
22 | pod 'Flipper-Folly', versions['Flipper-Folly'], :configuration => 'Debug'
23 | pod 'Flipper-Glog', versions['Flipper-Glog'], :configuration => 'Debug'
24 | pod 'Flipper-PeerTalk', versions['Flipper-PeerTalk'], :configuration => 'Debug'
25 | pod 'Flipper-RSocket', versions['Flipper-RSocket'], :configuration => 'Debug'
26 | pod 'FlipperKit/Core', versions['Flipper'], :configuration => 'Debug'
27 | pod 'FlipperKit/CppBridge', versions['Flipper'], :configuration => 'Debug'
28 | pod 'FlipperKit/FBCxxFollyDynamicConvert', versions['Flipper'], :configuration => 'Debug'
29 | pod 'FlipperKit/FBDefines', versions['Flipper'], :configuration => 'Debug'
30 | pod 'FlipperKit/FKPortForwarding', versions['Flipper'], :configuration => 'Debug'
31 | pod 'FlipperKit/FlipperKitHighlightOverlay', versions['Flipper'], :configuration => 'Debug'
32 | pod 'FlipperKit/FlipperKitLayoutTextSearchable', versions['Flipper'], :configuration => 'Debug'
33 | pod 'FlipperKit/FlipperKitNetworkPlugin', versions['Flipper'], :configuration => 'Debug'
34 | end
35 |
36 | # Post Install processing for Flipper
37 | def flipper_post_install(installer)
38 | installer.pods_project.targets.each do |target|
39 | if target.name == 'YogaKit'
40 | target.build_configurations.each do |config|
41 | config.build_settings['SWIFT_VERSION'] = '4.1'
42 | end
43 | end
44 | end
45 | end
46 |
47 | target 'PollsApp' do
48 | # Pods for PollsApp
49 | pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector"
50 | pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec"
51 | pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired"
52 | pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety"
53 | pod 'React', :path => '../node_modules/react-native/'
54 | pod 'React-Core', :path => '../node_modules/react-native/'
55 | pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules'
56 | pod 'React-Core/DevSupport', :path => '../node_modules/react-native/'
57 | pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
58 | pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
59 | pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
60 | pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
61 | pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
62 | pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
63 | pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
64 | pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
65 | pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
66 | pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/'
67 |
68 | pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
69 | pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
70 | pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
71 | pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
72 | pod 'ReactCommon/callinvoker', :path => "../node_modules/react-native/ReactCommon"
73 | pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon"
74 | pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga', :modular_headers => true
75 |
76 | pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
77 | pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
78 | pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
79 |
80 | use_native_modules!
81 |
82 | # Enables Flipper.
83 | #
84 | # Note that if you have use_frameworks! enabled, Flipper will not work and
85 | # you should disable these next few lines.
86 | add_flipper_pods!
87 | post_install do |installer|
88 | flipper_post_install(installer)
89 | end
90 | end
91 |
92 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "compilerOptions": {
4 | /* Basic Options */
5 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7 | "lib": ["es6"], /* Specify library files to be included in the compilation. */
8 | "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "sourceMap": true, /* Generates corresponding '.map' file. */
13 | // "outFile": "./", /* Concatenate and emit output to single file. */
14 | // "outDir": "./", /* Redirect output structure to the directory. */
15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16 | // "removeComments": true, /* Do not emit comments to output. */
17 | "noEmit": true, /* Do not emit outputs. */
18 | // "incremental": true, /* Enable incremental compilation */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 |
23 | /* Strict Type-Checking Options */
24 | "strict": true, /* Enable all strict type-checking options. */
25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
26 | // "strictNullChecks": true, /* Enable strict null checks. */
27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
31 |
32 | /* Additional Checks */
33 | // "noUnusedLocals": true, /* Report errors on unused locals. */
34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
37 |
38 | /* Module Resolution Options */
39 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
40 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
43 | // "typeRoots": [], /* List of folders to include type definitions from. */
44 | // "types": [], /* Type declaration files to be included in compilation. */
45 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
46 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
48 |
49 | /* Source Map Options */
50 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
51 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
54 |
55 | /* Experimental Options */
56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
58 | },
59 | "exclude": [
60 | "node_modules", "babel.config.js", "metro.config.js", "jest.config.js"
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=$((i+1))
158 | done
159 | case $i in
160 | (0) set -- ;;
161 | (1) set -- "$args0" ;;
162 | (2) set -- "$args0" "$args1" ;;
163 | (3) set -- "$args0" "$args1" "$args2" ;;
164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=$(save "$@")
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
185 | cd "$(dirname "$0")"
186 | fi
187 |
188 | exec "$JAVACMD" "$@"
189 |
--------------------------------------------------------------------------------
/src/store/sagas/__tests__/questionsSagas.test.ts:
--------------------------------------------------------------------------------
1 | import { runSaga, Saga } from 'redux-saga';
2 |
3 | import { APIClient, APIHelpers } from '../../../services';
4 | import {
5 | mockUrl,
6 | mockQuestionObjectResponse,
7 | mockQuestionAndChoiceObjects,
8 | mockQuestionBody,
9 | mockNextUrl,
10 | } from '../../../test/mocks';
11 | import { fetchQuestions, postQuestion } from '../questionsSagas';
12 | import { questionsActions } from '../../../store/slices/questionsSlice';
13 |
14 | describe('questionsSaga', () => {
15 | describe('fetchQuestions', () => {
16 | afterEach(() => {
17 | jest.clearAllMocks();
18 | });
19 |
20 | it('makes the api call with nextUrl if it is available', async () => {
21 | jest.spyOn(APIClient, 'getQuestions').mockImplementationOnce(() =>
22 | Promise.resolve({
23 | data: [mockQuestionObjectResponse],
24 | }),
25 | );
26 |
27 | const dispatched: ReturnType<
28 | typeof questionsActions.getQuestionsAndChoicesSucceeded
29 | >[] = [];
30 |
31 | await runSaga(
32 | {
33 | dispatch: (
34 | action: ReturnType<
35 | typeof questionsActions.getQuestionsAndChoicesSucceeded
36 | >,
37 | ) => dispatched.push(action),
38 | getState: () => ({
39 | api: { url: mockUrl },
40 | questions: { nextLink: mockNextUrl },
41 | }),
42 | },
43 | fetchQuestions as Saga,
44 | questionsActions.getQuestionsRequested(),
45 | );
46 |
47 | expect(APIClient.getQuestions).toBeCalledWith({
48 | url: mockNextUrl,
49 | });
50 | expect(dispatched).toEqual([
51 | questionsActions.getQuestionsAndChoicesSucceeded({
52 | questions: mockQuestionAndChoiceObjects.questionObjects,
53 | choices: mockQuestionAndChoiceObjects.choiceObjects,
54 | }),
55 | ]);
56 | });
57 |
58 | it('dispatches getQuestionsSucceeded action if api call succeeds', async () => {
59 | jest.spyOn(APIClient, 'getQuestions').mockImplementationOnce(() =>
60 | Promise.resolve({
61 | nextLink: mockNextUrl,
62 | data: [mockQuestionObjectResponse],
63 | }),
64 | );
65 |
66 | const dispatched: ReturnType<
67 | typeof questionsActions.getQuestionsAndChoicesSucceeded
68 | >[] = [];
69 |
70 | await runSaga(
71 | {
72 | dispatch: (
73 | action: ReturnType<
74 | typeof questionsActions.getQuestionsAndChoicesSucceeded
75 | >,
76 | ) => dispatched.push(action),
77 | getState: () => ({
78 | api: { url: mockUrl },
79 | questions: { nextLink: undefined },
80 | }),
81 | },
82 | fetchQuestions as Saga,
83 | questionsActions.getQuestionsRequested(),
84 | );
85 |
86 | expect(APIClient.getQuestions).toBeCalledWith({
87 | url: mockUrl,
88 | });
89 | expect(dispatched).toEqual([
90 | questionsActions.getQuestionsAndChoicesSucceeded({
91 | nextLink: mockNextUrl,
92 | questions: mockQuestionAndChoiceObjects.questionObjects,
93 | choices: mockQuestionAndChoiceObjects.choiceObjects,
94 | }),
95 | ]);
96 | });
97 |
98 | it('dispatches getQuestionsFailed action if api call fails', async () => {
99 | jest
100 | .spyOn(APIClient, 'getQuestions')
101 | .mockImplementationOnce(() => Promise.reject('Error'));
102 |
103 | const dispatched: ReturnType<
104 | typeof questionsActions.getQuestionsFailed
105 | >[] = [];
106 |
107 | await runSaga(
108 | {
109 | dispatch: (
110 | action: ReturnType,
111 | ) => dispatched.push(action),
112 | getState: () => ({
113 | api: { url: mockUrl },
114 | questions: { nextLink: undefined },
115 | }),
116 | },
117 | fetchQuestions as Saga,
118 | questionsActions.getQuestionsRequested(),
119 | );
120 |
121 | expect(APIClient.getQuestions).toBeCalledWith({
122 | url: mockUrl,
123 | });
124 | expect(dispatched).toEqual([
125 | questionsActions.getQuestionsFailed({
126 | errorMessage: APIHelpers.getGenericErrorMessage(),
127 | }),
128 | ]);
129 | });
130 | });
131 |
132 | describe('postQuestion', () => {
133 | afterEach(() => {
134 | jest.clearAllMocks();
135 | });
136 |
137 | it('dispatches postQuestionSucceeded action if api call succeeds', async () => {
138 | jest
139 | .spyOn(APIClient, 'postQuestion')
140 | .mockImplementationOnce(() =>
141 | Promise.resolve({ data: mockQuestionObjectResponse }),
142 | );
143 |
144 | const dispatched: ReturnType<
145 | typeof questionsActions.postQuestionSucceeded
146 | >[] = [];
147 |
148 | await runSaga(
149 | {
150 | dispatch: (
151 | action: ReturnType,
152 | ) => dispatched.push(action),
153 | getState: () => ({ api: { url: mockUrl } }),
154 | },
155 | postQuestion as Saga,
156 | questionsActions.postQuestionRequested({
157 | questionBody: mockQuestionBody,
158 | }),
159 | );
160 |
161 | expect(APIClient.postQuestion).toBeCalledWith({
162 | url: mockUrl,
163 | questionBody: mockQuestionBody,
164 | });
165 | expect(dispatched).toEqual([
166 | questionsActions.postQuestionSucceeded({
167 | questions: mockQuestionAndChoiceObjects.questionObjects,
168 | choices: mockQuestionAndChoiceObjects.choiceObjects,
169 | }),
170 | ]);
171 | });
172 |
173 | it('dispatches postQuestionFailed action if api call fails', async () => {
174 | jest
175 | .spyOn(APIClient, 'postQuestion')
176 | .mockImplementationOnce(() => Promise.reject('Error'));
177 |
178 | const dispatched: ReturnType<
179 | typeof questionsActions.postQuestionFailed
180 | >[] = [];
181 |
182 | await runSaga(
183 | {
184 | dispatch: (
185 | action: ReturnType,
186 | ) => dispatched.push(action),
187 | getState: () => ({ api: { url: mockUrl } }),
188 | },
189 | postQuestion as Saga,
190 | questionsActions.postQuestionRequested({
191 | questionBody: mockQuestionBody,
192 | }),
193 | );
194 |
195 | expect(APIClient.postQuestion).toBeCalledWith({
196 | url: mockUrl,
197 | questionBody: mockQuestionBody,
198 | });
199 | expect(dispatched).toEqual([
200 | questionsActions.postQuestionFailed({
201 | errorMessage: APIHelpers.getGenericErrorMessage(),
202 | }),
203 | ]);
204 | });
205 | });
206 | });
207 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 |
3 | import com.android.build.OutputFile
4 |
5 | /**
6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
7 | * and bundleReleaseJsAndAssets).
8 | * These basically call `react-native bundle` with the correct arguments during the Android build
9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
10 | * bundle directly from the development server. Below you can see all the possible configurations
11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the
12 | * `apply from: "../../node_modules/react-native/react.gradle"` line.
13 | *
14 | * project.ext.react = [
15 | * // the name of the generated asset file containing your JS bundle
16 | * bundleAssetName: "index.android.bundle",
17 | *
18 | * // the entry file for bundle generation. If none specified and
19 | * // "index.android.js" exists, it will be used. Otherwise "index.js" is
20 | * // default. Can be overridden with ENTRY_FILE environment variable.
21 | * entryFile: "index.android.js",
22 | *
23 | * // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
24 | * bundleCommand: "ram-bundle",
25 | *
26 | * // whether to bundle JS and assets in debug mode
27 | * bundleInDebug: false,
28 | *
29 | * // whether to bundle JS and assets in release mode
30 | * bundleInRelease: true,
31 | *
32 | * // whether to bundle JS and assets in another build variant (if configured).
33 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
34 | * // The configuration property can be in the following formats
35 | * // 'bundleIn${productFlavor}${buildType}'
36 | * // 'bundleIn${buildType}'
37 | * // bundleInFreeDebug: true,
38 | * // bundleInPaidRelease: true,
39 | * // bundleInBeta: true,
40 | *
41 | * // whether to disable dev mode in custom build variants (by default only disabled in release)
42 | * // for example: to disable dev mode in the staging build type (if configured)
43 | * devDisabledInStaging: true,
44 | * // The configuration property can be in the following formats
45 | * // 'devDisabledIn${productFlavor}${buildType}'
46 | * // 'devDisabledIn${buildType}'
47 | *
48 | * // the root of your project, i.e. where "package.json" lives
49 | * root: "../../",
50 | *
51 | * // where to put the JS bundle asset in debug mode
52 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
53 | *
54 | * // where to put the JS bundle asset in release mode
55 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release",
56 | *
57 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
58 | * // require('./image.png')), in debug mode
59 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
60 | *
61 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
62 | * // require('./image.png')), in release mode
63 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
64 | *
65 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means
66 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
67 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle
68 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
69 | * // for example, you might want to remove it from here.
70 | * inputExcludes: ["android/**", "ios/**"],
71 | *
72 | * // override which node gets called and with what additional arguments
73 | * nodeExecutableAndArgs: ["node"],
74 | *
75 | * // supply additional arguments to the packager
76 | * extraPackagerArgs: []
77 | * ]
78 | */
79 |
80 | project.ext.react = [
81 | enableHermes: true, // clean and rebuild if changing
82 | ]
83 |
84 | apply from: "../../node_modules/react-native/react.gradle"
85 |
86 | /**
87 | * Set this to true to create two separate APKs instead of one:
88 | * - An APK that only works on ARM devices
89 | * - An APK that only works on x86 devices
90 | * The advantage is the size of the APK is reduced by about 4MB.
91 | * Upload all the APKs to the Play Store and people will download
92 | * the correct one based on the CPU architecture of their device.
93 | */
94 | def enableSeparateBuildPerCPUArchitecture = false
95 |
96 | /**
97 | * Run Proguard to shrink the Java bytecode in release builds.
98 | */
99 | def enableProguardInReleaseBuilds = false
100 |
101 | /**
102 | * The preferred build flavor of JavaScriptCore.
103 | *
104 | * For example, to use the international variant, you can use:
105 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
106 | *
107 | * The international variant includes ICU i18n library and necessary data
108 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
109 | * give correct results when using with locales other than en-US. Note that
110 | * this variant is about 6MiB larger per architecture than default.
111 | */
112 | def jscFlavor = 'org.webkit:android-jsc:+'
113 |
114 | /**
115 | * Whether to enable the Hermes VM.
116 | *
117 | * This should be set on project.ext.react and mirrored here. If it is not set
118 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
119 | * and the benefits of using Hermes will therefore be sharply reduced.
120 | */
121 | def enableHermes = project.ext.react.get("enableHermes", false);
122 |
123 | android {
124 | compileSdkVersion rootProject.ext.compileSdkVersion
125 |
126 | compileOptions {
127 | sourceCompatibility JavaVersion.VERSION_1_8
128 | targetCompatibility JavaVersion.VERSION_1_8
129 | }
130 |
131 | defaultConfig {
132 | applicationId "com.pollsapp"
133 | minSdkVersion rootProject.ext.minSdkVersion
134 | targetSdkVersion rootProject.ext.targetSdkVersion
135 | versionCode 1
136 | versionName "1.0"
137 | }
138 | splits {
139 | abi {
140 | reset()
141 | enable enableSeparateBuildPerCPUArchitecture
142 | universalApk false // If true, also generate a universal APK
143 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
144 | }
145 | }
146 | signingConfigs {
147 | debug {
148 | storeFile file('debug.keystore')
149 | storePassword 'android'
150 | keyAlias 'androiddebugkey'
151 | keyPassword 'android'
152 | }
153 | }
154 | buildTypes {
155 | debug {
156 | signingConfig signingConfigs.debug
157 | }
158 | release {
159 | // Caution! In production, you need to generate your own keystore file.
160 | // see https://facebook.github.io/react-native/docs/signed-apk-android.
161 | signingConfig signingConfigs.debug
162 | minifyEnabled enableProguardInReleaseBuilds
163 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
164 | }
165 | }
166 |
167 | packagingOptions {
168 | pickFirst "lib/armeabi-v7a/libc++_shared.so"
169 | pickFirst "lib/arm64-v8a/libc++_shared.so"
170 | pickFirst "lib/x86/libc++_shared.so"
171 | pickFirst "lib/x86_64/libc++_shared.so"
172 | }
173 |
174 | // applicationVariants are e.g. debug, release
175 | applicationVariants.all { variant ->
176 | variant.outputs.each { output ->
177 | // For each separate APK per architecture, set a unique version code as described here:
178 | // https://developer.android.com/studio/build/configure-apk-splits.html
179 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
180 | def abi = output.getFilter(OutputFile.ABI)
181 | if (abi != null) { // null for the universal-debug, universal-release variants
182 | output.versionCodeOverride =
183 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
184 | }
185 |
186 | }
187 | }
188 | }
189 |
190 | dependencies {
191 | implementation fileTree(dir: "libs", include: ["*.jar"])
192 | //noinspection GradleDynamicVersion
193 | implementation "com.facebook.react:react-native:+" // From node_modules
194 |
195 | implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
196 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
197 |
198 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
199 | exclude group:'com.facebook.fbjni'
200 | }
201 |
202 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
203 | exclude group:'com.facebook.flipper'
204 | }
205 |
206 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
207 | exclude group:'com.facebook.flipper'
208 | }
209 |
210 | if (enableHermes) {
211 | def hermesPath = "../../node_modules/hermes-engine/android/";
212 | debugImplementation files(hermesPath + "hermes-debug.aar")
213 | releaseImplementation files(hermesPath + "hermes-release.aar")
214 | } else {
215 | implementation jscFlavor
216 | }
217 | }
218 |
219 | // Run this once to be able to run the application with BUCK
220 | // puts all compile dependencies into folder libs for BUCK to use
221 | task copyDownloadableDepsToLibs(type: Copy) {
222 | from configurations.compile
223 | into 'libs'
224 | }
225 |
226 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
227 |
--------------------------------------------------------------------------------
/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - boost-for-react-native (1.63.0)
3 | - BVLinearGradient (2.5.6):
4 | - React
5 | - CocoaAsyncSocket (7.6.4)
6 | - CocoaLibEvent (1.0.0)
7 | - DoubleConversion (1.1.6)
8 | - FBLazyVector (0.62.2)
9 | - FBReactNativeSpec (0.62.2):
10 | - Folly (= 2018.10.22.00)
11 | - RCTRequired (= 0.62.2)
12 | - RCTTypeSafety (= 0.62.2)
13 | - React-Core (= 0.62.2)
14 | - React-jsi (= 0.62.2)
15 | - ReactCommon/turbomodule/core (= 0.62.2)
16 | - Flipper (0.33.1):
17 | - Flipper-Folly (~> 2.1)
18 | - Flipper-RSocket (~> 1.0)
19 | - Flipper-DoubleConversion (1.1.7)
20 | - Flipper-Folly (2.2.0):
21 | - boost-for-react-native
22 | - CocoaLibEvent (~> 1.0)
23 | - Flipper-DoubleConversion
24 | - Flipper-Glog
25 | - OpenSSL-Universal (= 1.0.2.19)
26 | - Flipper-Glog (0.3.6)
27 | - Flipper-PeerTalk (0.0.4)
28 | - Flipper-RSocket (1.1.0):
29 | - Flipper-Folly (~> 2.2)
30 | - FlipperKit (0.33.1):
31 | - FlipperKit/Core (= 0.33.1)
32 | - FlipperKit/Core (0.33.1):
33 | - Flipper (~> 0.33.1)
34 | - FlipperKit/CppBridge
35 | - FlipperKit/FBCxxFollyDynamicConvert
36 | - FlipperKit/FBDefines
37 | - FlipperKit/FKPortForwarding
38 | - FlipperKit/CppBridge (0.33.1):
39 | - Flipper (~> 0.33.1)
40 | - FlipperKit/FBCxxFollyDynamicConvert (0.33.1):
41 | - Flipper-Folly (~> 2.1)
42 | - FlipperKit/FBDefines (0.33.1)
43 | - FlipperKit/FKPortForwarding (0.33.1):
44 | - CocoaAsyncSocket (~> 7.6)
45 | - Flipper-PeerTalk (~> 0.0.4)
46 | - FlipperKit/FlipperKitHighlightOverlay (0.33.1)
47 | - FlipperKit/FlipperKitLayoutPlugin (0.33.1):
48 | - FlipperKit/Core
49 | - FlipperKit/FlipperKitHighlightOverlay
50 | - FlipperKit/FlipperKitLayoutTextSearchable
51 | - YogaKit (~> 1.18)
52 | - FlipperKit/FlipperKitLayoutTextSearchable (0.33.1)
53 | - FlipperKit/FlipperKitNetworkPlugin (0.33.1):
54 | - FlipperKit/Core
55 | - FlipperKit/FlipperKitReactPlugin (0.33.1):
56 | - FlipperKit/Core
57 | - FlipperKit/FlipperKitUserDefaultsPlugin (0.33.1):
58 | - FlipperKit/Core
59 | - FlipperKit/SKIOSNetworkPlugin (0.33.1):
60 | - FlipperKit/Core
61 | - FlipperKit/FlipperKitNetworkPlugin
62 | - Folly (2018.10.22.00):
63 | - boost-for-react-native
64 | - DoubleConversion
65 | - Folly/Default (= 2018.10.22.00)
66 | - glog
67 | - Folly/Default (2018.10.22.00):
68 | - boost-for-react-native
69 | - DoubleConversion
70 | - glog
71 | - glog (0.3.5)
72 | - OpenSSL-Universal (1.0.2.19):
73 | - OpenSSL-Universal/Static (= 1.0.2.19)
74 | - OpenSSL-Universal/Static (1.0.2.19)
75 | - RCTRequired (0.62.2)
76 | - RCTTypeSafety (0.62.2):
77 | - FBLazyVector (= 0.62.2)
78 | - Folly (= 2018.10.22.00)
79 | - RCTRequired (= 0.62.2)
80 | - React-Core (= 0.62.2)
81 | - React (0.62.2):
82 | - React-Core (= 0.62.2)
83 | - React-Core/DevSupport (= 0.62.2)
84 | - React-Core/RCTWebSocket (= 0.62.2)
85 | - React-RCTActionSheet (= 0.62.2)
86 | - React-RCTAnimation (= 0.62.2)
87 | - React-RCTBlob (= 0.62.2)
88 | - React-RCTImage (= 0.62.2)
89 | - React-RCTLinking (= 0.62.2)
90 | - React-RCTNetwork (= 0.62.2)
91 | - React-RCTSettings (= 0.62.2)
92 | - React-RCTText (= 0.62.2)
93 | - React-RCTVibration (= 0.62.2)
94 | - React-Core (0.62.2):
95 | - Folly (= 2018.10.22.00)
96 | - glog
97 | - React-Core/Default (= 0.62.2)
98 | - React-cxxreact (= 0.62.2)
99 | - React-jsi (= 0.62.2)
100 | - React-jsiexecutor (= 0.62.2)
101 | - Yoga
102 | - React-Core/CoreModulesHeaders (0.62.2):
103 | - Folly (= 2018.10.22.00)
104 | - glog
105 | - React-Core/Default
106 | - React-cxxreact (= 0.62.2)
107 | - React-jsi (= 0.62.2)
108 | - React-jsiexecutor (= 0.62.2)
109 | - Yoga
110 | - React-Core/Default (0.62.2):
111 | - Folly (= 2018.10.22.00)
112 | - glog
113 | - React-cxxreact (= 0.62.2)
114 | - React-jsi (= 0.62.2)
115 | - React-jsiexecutor (= 0.62.2)
116 | - Yoga
117 | - React-Core/DevSupport (0.62.2):
118 | - Folly (= 2018.10.22.00)
119 | - glog
120 | - React-Core/Default (= 0.62.2)
121 | - React-Core/RCTWebSocket (= 0.62.2)
122 | - React-cxxreact (= 0.62.2)
123 | - React-jsi (= 0.62.2)
124 | - React-jsiexecutor (= 0.62.2)
125 | - React-jsinspector (= 0.62.2)
126 | - Yoga
127 | - React-Core/RCTActionSheetHeaders (0.62.2):
128 | - Folly (= 2018.10.22.00)
129 | - glog
130 | - React-Core/Default
131 | - React-cxxreact (= 0.62.2)
132 | - React-jsi (= 0.62.2)
133 | - React-jsiexecutor (= 0.62.2)
134 | - Yoga
135 | - React-Core/RCTAnimationHeaders (0.62.2):
136 | - Folly (= 2018.10.22.00)
137 | - glog
138 | - React-Core/Default
139 | - React-cxxreact (= 0.62.2)
140 | - React-jsi (= 0.62.2)
141 | - React-jsiexecutor (= 0.62.2)
142 | - Yoga
143 | - React-Core/RCTBlobHeaders (0.62.2):
144 | - Folly (= 2018.10.22.00)
145 | - glog
146 | - React-Core/Default
147 | - React-cxxreact (= 0.62.2)
148 | - React-jsi (= 0.62.2)
149 | - React-jsiexecutor (= 0.62.2)
150 | - Yoga
151 | - React-Core/RCTImageHeaders (0.62.2):
152 | - Folly (= 2018.10.22.00)
153 | - glog
154 | - React-Core/Default
155 | - React-cxxreact (= 0.62.2)
156 | - React-jsi (= 0.62.2)
157 | - React-jsiexecutor (= 0.62.2)
158 | - Yoga
159 | - React-Core/RCTLinkingHeaders (0.62.2):
160 | - Folly (= 2018.10.22.00)
161 | - glog
162 | - React-Core/Default
163 | - React-cxxreact (= 0.62.2)
164 | - React-jsi (= 0.62.2)
165 | - React-jsiexecutor (= 0.62.2)
166 | - Yoga
167 | - React-Core/RCTNetworkHeaders (0.62.2):
168 | - Folly (= 2018.10.22.00)
169 | - glog
170 | - React-Core/Default
171 | - React-cxxreact (= 0.62.2)
172 | - React-jsi (= 0.62.2)
173 | - React-jsiexecutor (= 0.62.2)
174 | - Yoga
175 | - React-Core/RCTSettingsHeaders (0.62.2):
176 | - Folly (= 2018.10.22.00)
177 | - glog
178 | - React-Core/Default
179 | - React-cxxreact (= 0.62.2)
180 | - React-jsi (= 0.62.2)
181 | - React-jsiexecutor (= 0.62.2)
182 | - Yoga
183 | - React-Core/RCTTextHeaders (0.62.2):
184 | - Folly (= 2018.10.22.00)
185 | - glog
186 | - React-Core/Default
187 | - React-cxxreact (= 0.62.2)
188 | - React-jsi (= 0.62.2)
189 | - React-jsiexecutor (= 0.62.2)
190 | - Yoga
191 | - React-Core/RCTVibrationHeaders (0.62.2):
192 | - Folly (= 2018.10.22.00)
193 | - glog
194 | - React-Core/Default
195 | - React-cxxreact (= 0.62.2)
196 | - React-jsi (= 0.62.2)
197 | - React-jsiexecutor (= 0.62.2)
198 | - Yoga
199 | - React-Core/RCTWebSocket (0.62.2):
200 | - Folly (= 2018.10.22.00)
201 | - glog
202 | - React-Core/Default (= 0.62.2)
203 | - React-cxxreact (= 0.62.2)
204 | - React-jsi (= 0.62.2)
205 | - React-jsiexecutor (= 0.62.2)
206 | - Yoga
207 | - React-CoreModules (0.62.2):
208 | - FBReactNativeSpec (= 0.62.2)
209 | - Folly (= 2018.10.22.00)
210 | - RCTTypeSafety (= 0.62.2)
211 | - React-Core/CoreModulesHeaders (= 0.62.2)
212 | - React-RCTImage (= 0.62.2)
213 | - ReactCommon/turbomodule/core (= 0.62.2)
214 | - React-cxxreact (0.62.2):
215 | - boost-for-react-native (= 1.63.0)
216 | - DoubleConversion
217 | - Folly (= 2018.10.22.00)
218 | - glog
219 | - React-jsinspector (= 0.62.2)
220 | - React-jsi (0.62.2):
221 | - boost-for-react-native (= 1.63.0)
222 | - DoubleConversion
223 | - Folly (= 2018.10.22.00)
224 | - glog
225 | - React-jsi/Default (= 0.62.2)
226 | - React-jsi/Default (0.62.2):
227 | - boost-for-react-native (= 1.63.0)
228 | - DoubleConversion
229 | - Folly (= 2018.10.22.00)
230 | - glog
231 | - React-jsiexecutor (0.62.2):
232 | - DoubleConversion
233 | - Folly (= 2018.10.22.00)
234 | - glog
235 | - React-cxxreact (= 0.62.2)
236 | - React-jsi (= 0.62.2)
237 | - React-jsinspector (0.62.2)
238 | - react-native-safe-area-context (3.0.2):
239 | - React
240 | - React-RCTActionSheet (0.62.2):
241 | - React-Core/RCTActionSheetHeaders (= 0.62.2)
242 | - React-RCTAnimation (0.62.2):
243 | - FBReactNativeSpec (= 0.62.2)
244 | - Folly (= 2018.10.22.00)
245 | - RCTTypeSafety (= 0.62.2)
246 | - React-Core/RCTAnimationHeaders (= 0.62.2)
247 | - ReactCommon/turbomodule/core (= 0.62.2)
248 | - React-RCTBlob (0.62.2):
249 | - FBReactNativeSpec (= 0.62.2)
250 | - Folly (= 2018.10.22.00)
251 | - React-Core/RCTBlobHeaders (= 0.62.2)
252 | - React-Core/RCTWebSocket (= 0.62.2)
253 | - React-jsi (= 0.62.2)
254 | - React-RCTNetwork (= 0.62.2)
255 | - ReactCommon/turbomodule/core (= 0.62.2)
256 | - React-RCTImage (0.62.2):
257 | - FBReactNativeSpec (= 0.62.2)
258 | - Folly (= 2018.10.22.00)
259 | - RCTTypeSafety (= 0.62.2)
260 | - React-Core/RCTImageHeaders (= 0.62.2)
261 | - React-RCTNetwork (= 0.62.2)
262 | - ReactCommon/turbomodule/core (= 0.62.2)
263 | - React-RCTLinking (0.62.2):
264 | - FBReactNativeSpec (= 0.62.2)
265 | - React-Core/RCTLinkingHeaders (= 0.62.2)
266 | - ReactCommon/turbomodule/core (= 0.62.2)
267 | - React-RCTNetwork (0.62.2):
268 | - FBReactNativeSpec (= 0.62.2)
269 | - Folly (= 2018.10.22.00)
270 | - RCTTypeSafety (= 0.62.2)
271 | - React-Core/RCTNetworkHeaders (= 0.62.2)
272 | - ReactCommon/turbomodule/core (= 0.62.2)
273 | - React-RCTSettings (0.62.2):
274 | - FBReactNativeSpec (= 0.62.2)
275 | - Folly (= 2018.10.22.00)
276 | - RCTTypeSafety (= 0.62.2)
277 | - React-Core/RCTSettingsHeaders (= 0.62.2)
278 | - ReactCommon/turbomodule/core (= 0.62.2)
279 | - React-RCTText (0.62.2):
280 | - React-Core/RCTTextHeaders (= 0.62.2)
281 | - React-RCTVibration (0.62.2):
282 | - FBReactNativeSpec (= 0.62.2)
283 | - Folly (= 2018.10.22.00)
284 | - React-Core/RCTVibrationHeaders (= 0.62.2)
285 | - ReactCommon/turbomodule/core (= 0.62.2)
286 | - ReactCommon/callinvoker (0.62.2):
287 | - DoubleConversion
288 | - Folly (= 2018.10.22.00)
289 | - glog
290 | - React-cxxreact (= 0.62.2)
291 | - ReactCommon/turbomodule/core (0.62.2):
292 | - DoubleConversion
293 | - Folly (= 2018.10.22.00)
294 | - glog
295 | - React-Core (= 0.62.2)
296 | - React-cxxreact (= 0.62.2)
297 | - React-jsi (= 0.62.2)
298 | - ReactCommon/callinvoker (= 0.62.2)
299 | - RNCMaskedView (0.1.10):
300 | - React
301 | - RNGestureHandler (1.6.1):
302 | - React
303 | - RNReanimated (1.9.0):
304 | - React
305 | - RNScreens (2.8.0):
306 | - React
307 | - Yoga (1.14.0)
308 | - YogaKit (1.18.1):
309 | - Yoga (~> 1.14)
310 |
311 | DEPENDENCIES:
312 | - BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
313 | - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
314 | - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
315 | - FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`)
316 | - Flipper (~> 0.33.1)
317 | - Flipper-DoubleConversion (= 1.1.7)
318 | - Flipper-Folly (~> 2.1)
319 | - Flipper-Glog (= 0.3.6)
320 | - Flipper-PeerTalk (~> 0.0.4)
321 | - Flipper-RSocket (~> 1.0)
322 | - FlipperKit (~> 0.33.1)
323 | - FlipperKit/Core (~> 0.33.1)
324 | - FlipperKit/CppBridge (~> 0.33.1)
325 | - FlipperKit/FBCxxFollyDynamicConvert (~> 0.33.1)
326 | - FlipperKit/FBDefines (~> 0.33.1)
327 | - FlipperKit/FKPortForwarding (~> 0.33.1)
328 | - FlipperKit/FlipperKitHighlightOverlay (~> 0.33.1)
329 | - FlipperKit/FlipperKitLayoutPlugin (~> 0.33.1)
330 | - FlipperKit/FlipperKitLayoutTextSearchable (~> 0.33.1)
331 | - FlipperKit/FlipperKitNetworkPlugin (~> 0.33.1)
332 | - FlipperKit/FlipperKitReactPlugin (~> 0.33.1)
333 | - FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.33.1)
334 | - FlipperKit/SKIOSNetworkPlugin (~> 0.33.1)
335 | - Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`)
336 | - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
337 | - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
338 | - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
339 | - React (from `../node_modules/react-native/`)
340 | - React-Core (from `../node_modules/react-native/`)
341 | - React-Core/DevSupport (from `../node_modules/react-native/`)
342 | - React-Core/RCTWebSocket (from `../node_modules/react-native/`)
343 | - React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
344 | - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
345 | - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
346 | - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
347 | - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
348 | - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
349 | - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
350 | - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
351 | - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`)
352 | - React-RCTImage (from `../node_modules/react-native/Libraries/Image`)
353 | - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`)
354 | - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`)
355 | - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`)
356 | - React-RCTText (from `../node_modules/react-native/Libraries/Text`)
357 | - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
358 | - ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`)
359 | - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
360 | - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
361 | - RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
362 | - RNReanimated (from `../node_modules/react-native-reanimated`)
363 | - RNScreens (from `../node_modules/react-native-screens`)
364 | - Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
365 |
366 | SPEC REPOS:
367 | trunk:
368 | - boost-for-react-native
369 | - CocoaAsyncSocket
370 | - CocoaLibEvent
371 | - Flipper
372 | - Flipper-DoubleConversion
373 | - Flipper-Folly
374 | - Flipper-Glog
375 | - Flipper-PeerTalk
376 | - Flipper-RSocket
377 | - FlipperKit
378 | - OpenSSL-Universal
379 | - YogaKit
380 |
381 | EXTERNAL SOURCES:
382 | BVLinearGradient:
383 | :path: "../node_modules/react-native-linear-gradient"
384 | DoubleConversion:
385 | :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
386 | FBLazyVector:
387 | :path: "../node_modules/react-native/Libraries/FBLazyVector"
388 | FBReactNativeSpec:
389 | :path: "../node_modules/react-native/Libraries/FBReactNativeSpec"
390 | Folly:
391 | :podspec: "../node_modules/react-native/third-party-podspecs/Folly.podspec"
392 | glog:
393 | :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
394 | RCTRequired:
395 | :path: "../node_modules/react-native/Libraries/RCTRequired"
396 | RCTTypeSafety:
397 | :path: "../node_modules/react-native/Libraries/TypeSafety"
398 | React:
399 | :path: "../node_modules/react-native/"
400 | React-Core:
401 | :path: "../node_modules/react-native/"
402 | React-CoreModules:
403 | :path: "../node_modules/react-native/React/CoreModules"
404 | React-cxxreact:
405 | :path: "../node_modules/react-native/ReactCommon/cxxreact"
406 | React-jsi:
407 | :path: "../node_modules/react-native/ReactCommon/jsi"
408 | React-jsiexecutor:
409 | :path: "../node_modules/react-native/ReactCommon/jsiexecutor"
410 | React-jsinspector:
411 | :path: "../node_modules/react-native/ReactCommon/jsinspector"
412 | react-native-safe-area-context:
413 | :path: "../node_modules/react-native-safe-area-context"
414 | React-RCTActionSheet:
415 | :path: "../node_modules/react-native/Libraries/ActionSheetIOS"
416 | React-RCTAnimation:
417 | :path: "../node_modules/react-native/Libraries/NativeAnimation"
418 | React-RCTBlob:
419 | :path: "../node_modules/react-native/Libraries/Blob"
420 | React-RCTImage:
421 | :path: "../node_modules/react-native/Libraries/Image"
422 | React-RCTLinking:
423 | :path: "../node_modules/react-native/Libraries/LinkingIOS"
424 | React-RCTNetwork:
425 | :path: "../node_modules/react-native/Libraries/Network"
426 | React-RCTSettings:
427 | :path: "../node_modules/react-native/Libraries/Settings"
428 | React-RCTText:
429 | :path: "../node_modules/react-native/Libraries/Text"
430 | React-RCTVibration:
431 | :path: "../node_modules/react-native/Libraries/Vibration"
432 | ReactCommon:
433 | :path: "../node_modules/react-native/ReactCommon"
434 | RNCMaskedView:
435 | :path: "../node_modules/@react-native-community/masked-view"
436 | RNGestureHandler:
437 | :path: "../node_modules/react-native-gesture-handler"
438 | RNReanimated:
439 | :path: "../node_modules/react-native-reanimated"
440 | RNScreens:
441 | :path: "../node_modules/react-native-screens"
442 | Yoga:
443 | :path: "../node_modules/react-native/ReactCommon/yoga"
444 |
445 | SPEC CHECKSUMS:
446 | boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
447 | BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
448 | CocoaAsyncSocket: 694058e7c0ed05a9e217d1b3c7ded962f4180845
449 | CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f
450 | DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
451 | FBLazyVector: 4aab18c93cd9546e4bfed752b4084585eca8b245
452 | FBReactNativeSpec: 5465d51ccfeecb7faa12f9ae0024f2044ce4044e
453 | Flipper: 6c1f484f9a88d30ab3e272800d53688439e50f69
454 | Flipper-DoubleConversion: 38631e41ef4f9b12861c67d17cb5518d06badc41
455 | Flipper-Folly: c12092ea368353b58e992843a990a3225d4533c3
456 | Flipper-Glog: 1dfd6abf1e922806c52ceb8701a3599a79a200a6
457 | Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9
458 | Flipper-RSocket: 64e7431a55835eb953b0bf984ef3b90ae9fdddd7
459 | FlipperKit: 6dc9b8f4ef60d9e5ded7f0264db299c91f18832e
460 | Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
461 | glog: 1f3da668190260b06b429bb211bfbee5cd790c28
462 | OpenSSL-Universal: 8b48cc0d10c1b2923617dfe5c178aa9ed2689355
463 | RCTRequired: cec6a34b3ac8a9915c37e7e4ad3aa74726ce4035
464 | RCTTypeSafety: 93006131180074cffa227a1075802c89a49dd4ce
465 | React: 29a8b1a02bd764fb7644ef04019270849b9a7ac3
466 | React-Core: b12bffb3f567fdf99510acb716ef1abd426e0e05
467 | React-CoreModules: 4a9b87bbe669d6c3173c0132c3328e3b000783d0
468 | React-cxxreact: e65f9c2ba0ac5be946f53548c1aaaee5873a8103
469 | React-jsi: b6dc94a6a12ff98e8877287a0b7620d365201161
470 | React-jsiexecutor: 1540d1c01bb493ae3124ed83351b1b6a155db7da
471 | React-jsinspector: 512e560d0e985d0e8c479a54a4e5c147a9c83493
472 | react-native-safe-area-context: b11a34881faac509cad5578726c98161ad4d275c
473 | React-RCTActionSheet: f41ea8a811aac770e0cc6e0ad6b270c644ea8b7c
474 | React-RCTAnimation: 49ab98b1c1ff4445148b72a3d61554138565bad0
475 | React-RCTBlob: a332773f0ebc413a0ce85942a55b064471587a71
476 | React-RCTImage: e70be9b9c74fe4e42d0005f42cace7981c994ac3
477 | React-RCTLinking: c1b9739a88d56ecbec23b7f63650e44672ab2ad2
478 | React-RCTNetwork: 73138b6f45e5a2768ad93f3d57873c2a18d14b44
479 | React-RCTSettings: 6e3738a87e21b39a8cb08d627e68c44acf1e325a
480 | React-RCTText: fae545b10cfdb3d247c36c56f61a94cfd6dba41d
481 | React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256
482 | ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3
483 | RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f
484 | RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38
485 | RNReanimated: b5ccb50650ba06f6e749c7c329a1bc3ae0c88b43
486 | RNScreens: 62211832af51e0aebcf6e8c36bcf7dd65592f244
487 | Yoga: 3ebccbdd559724312790e7742142d062476b698e
488 | YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
489 |
490 | PODFILE CHECKSUM: de64269f70978004d0f32bcf7e36aba2b9025b98
491 |
492 | COCOAPODS: 1.8.4
493 |
--------------------------------------------------------------------------------
/ios/PollsApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 003E13E4DB52D409C7365A7F /* libPods-PollsApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 859CFAB7B5740D697AB053A4 /* libPods-PollsApp.a */; };
11 | 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
12 | 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
13 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
14 | 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXFileReference section */
18 | 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; };
19 | 05E7432CC241106C9DDC61CB /* Pods-PollsApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PollsApp.release.xcconfig"; path = "Target Support Files/Pods-PollsApp/Pods-PollsApp.release.xcconfig"; sourceTree = ""; };
20 | 13B07F961A680F5B00A75B9A /* PollsApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PollsApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
21 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = PollsApp/AppDelegate.h; sourceTree = ""; };
22 | 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = PollsApp/AppDelegate.m; sourceTree = ""; };
23 | 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; };
24 | 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = PollsApp/Images.xcassets; sourceTree = ""; };
25 | 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = PollsApp/Info.plist; sourceTree = ""; };
26 | 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = PollsApp/main.m; sourceTree = ""; };
27 | 40262907E515AF868B91782D /* Pods-PollsApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PollsApp.debug.xcconfig"; path = "Target Support Files/Pods-PollsApp/Pods-PollsApp.debug.xcconfig"; sourceTree = ""; };
28 | 859CFAB7B5740D697AB053A4 /* libPods-PollsApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-PollsApp.a"; sourceTree = BUILT_PRODUCTS_DIR; };
29 | AB432F4861DA713A8F25365D /* Pods-PollsApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PollsApp.debug.xcconfig"; path = "Target Support Files/Pods-PollsApp/Pods-PollsApp.debug.xcconfig"; sourceTree = ""; };
30 | C8C1F1DE6D831BA326799B1C /* Pods-PollsApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PollsApp.release.xcconfig"; path = "Target Support Files/Pods-PollsApp/Pods-PollsApp.release.xcconfig"; sourceTree = ""; };
31 | ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
32 | ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
33 | /* End PBXFileReference section */
34 |
35 | /* Begin PBXFrameworksBuildPhase section */
36 | 13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
37 | isa = PBXFrameworksBuildPhase;
38 | buildActionMask = 2147483647;
39 | files = (
40 | 003E13E4DB52D409C7365A7F /* libPods-PollsApp.a in Frameworks */,
41 | );
42 | runOnlyForDeploymentPostprocessing = 0;
43 | };
44 | /* End PBXFrameworksBuildPhase section */
45 |
46 | /* Begin PBXGroup section */
47 | 13B07FAE1A68108700A75B9A /* PollsApp */ = {
48 | isa = PBXGroup;
49 | children = (
50 | 008F07F21AC5B25A0029DE68 /* main.jsbundle */,
51 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */,
52 | 13B07FB01A68108700A75B9A /* AppDelegate.m */,
53 | 13B07FB51A68108700A75B9A /* Images.xcassets */,
54 | 13B07FB61A68108700A75B9A /* Info.plist */,
55 | 13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
56 | 13B07FB71A68108700A75B9A /* main.m */,
57 | );
58 | name = PollsApp;
59 | sourceTree = "";
60 | };
61 | 2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
62 | isa = PBXGroup;
63 | children = (
64 | ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
65 | ED2971642150620600B7C4FE /* JavaScriptCore.framework */,
66 | 859CFAB7B5740D697AB053A4 /* libPods-PollsApp.a */,
67 | );
68 | name = Frameworks;
69 | sourceTree = "";
70 | };
71 | 832341AE1AAA6A7D00B99B32 /* Libraries */ = {
72 | isa = PBXGroup;
73 | children = (
74 | );
75 | name = Libraries;
76 | sourceTree = "";
77 | };
78 | 83CBB9F61A601CBA00E9B192 = {
79 | isa = PBXGroup;
80 | children = (
81 | 13B07FAE1A68108700A75B9A /* PollsApp */,
82 | 832341AE1AAA6A7D00B99B32 /* Libraries */,
83 | 83CBBA001A601CBA00E9B192 /* Products */,
84 | 2D16E6871FA4F8E400B85C8A /* Frameworks */,
85 | F5B61AACDA06C96E0E0FB68B /* Pods */,
86 | );
87 | indentWidth = 2;
88 | sourceTree = "";
89 | tabWidth = 2;
90 | usesTabs = 0;
91 | };
92 | 83CBBA001A601CBA00E9B192 /* Products */ = {
93 | isa = PBXGroup;
94 | children = (
95 | 13B07F961A680F5B00A75B9A /* PollsApp.app */,
96 | );
97 | name = Products;
98 | sourceTree = "";
99 | };
100 | F5B61AACDA06C96E0E0FB68B /* Pods */ = {
101 | isa = PBXGroup;
102 | children = (
103 | AB432F4861DA713A8F25365D /* Pods-PollsApp.debug.xcconfig */,
104 | C8C1F1DE6D831BA326799B1C /* Pods-PollsApp.release.xcconfig */,
105 | 40262907E515AF868B91782D /* Pods-PollsApp.debug.xcconfig */,
106 | 05E7432CC241106C9DDC61CB /* Pods-PollsApp.release.xcconfig */,
107 | );
108 | path = Pods;
109 | sourceTree = "";
110 | };
111 | /* End PBXGroup section */
112 |
113 | /* Begin PBXNativeTarget section */
114 | 13B07F861A680F5B00A75B9A /* PollsApp */ = {
115 | isa = PBXNativeTarget;
116 | buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "PollsApp" */;
117 | buildPhases = (
118 | A482DA8871E779BB9C7DE4C1 /* [CP] Check Pods Manifest.lock */,
119 | FD10A7F022414F080027D42C /* Start Packager */,
120 | 13B07F871A680F5B00A75B9A /* Sources */,
121 | 13B07F8C1A680F5B00A75B9A /* Frameworks */,
122 | 13B07F8E1A680F5B00A75B9A /* Resources */,
123 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
124 | );
125 | buildRules = (
126 | );
127 | dependencies = (
128 | );
129 | name = PollsApp;
130 | productName = PollsApp;
131 | productReference = 13B07F961A680F5B00A75B9A /* PollsApp.app */;
132 | productType = "com.apple.product-type.application";
133 | };
134 | /* End PBXNativeTarget section */
135 |
136 | /* Begin PBXProject section */
137 | 83CBB9F71A601CBA00E9B192 /* Project object */ = {
138 | isa = PBXProject;
139 | attributes = {
140 | LastUpgradeCheck = 1130;
141 | TargetAttributes = {
142 | 13B07F861A680F5B00A75B9A = {
143 | LastSwiftMigration = 1120;
144 | };
145 | };
146 | };
147 | buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "PollsApp" */;
148 | compatibilityVersion = "Xcode 3.2";
149 | developmentRegion = en;
150 | hasScannedForEncodings = 0;
151 | knownRegions = (
152 | en,
153 | Base,
154 | );
155 | mainGroup = 83CBB9F61A601CBA00E9B192;
156 | productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
157 | projectDirPath = "";
158 | projectRoot = "";
159 | targets = (
160 | 13B07F861A680F5B00A75B9A /* PollsApp */,
161 | );
162 | };
163 | /* End PBXProject section */
164 |
165 | /* Begin PBXResourcesBuildPhase section */
166 | 13B07F8E1A680F5B00A75B9A /* Resources */ = {
167 | isa = PBXResourcesBuildPhase;
168 | buildActionMask = 2147483647;
169 | files = (
170 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
171 | 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
172 | );
173 | runOnlyForDeploymentPostprocessing = 0;
174 | };
175 | /* End PBXResourcesBuildPhase section */
176 |
177 | /* Begin PBXShellScriptBuildPhase section */
178 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
179 | isa = PBXShellScriptBuildPhase;
180 | buildActionMask = 2147483647;
181 | files = (
182 | );
183 | inputPaths = (
184 | );
185 | name = "Bundle React Native code and images";
186 | outputPaths = (
187 | );
188 | runOnlyForDeploymentPostprocessing = 0;
189 | shellPath = /bin/sh;
190 | shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh";
191 | };
192 | A482DA8871E779BB9C7DE4C1 /* [CP] Check Pods Manifest.lock */ = {
193 | isa = PBXShellScriptBuildPhase;
194 | buildActionMask = 2147483647;
195 | files = (
196 | );
197 | inputFileListPaths = (
198 | );
199 | inputPaths = (
200 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
201 | "${PODS_ROOT}/Manifest.lock",
202 | );
203 | name = "[CP] Check Pods Manifest.lock";
204 | outputFileListPaths = (
205 | );
206 | outputPaths = (
207 | "$(DERIVED_FILE_DIR)/Pods-PollsApp-checkManifestLockResult.txt",
208 | );
209 | runOnlyForDeploymentPostprocessing = 0;
210 | shellPath = /bin/sh;
211 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
212 | showEnvVarsInLog = 0;
213 | };
214 | FD10A7F022414F080027D42C /* Start Packager */ = {
215 | isa = PBXShellScriptBuildPhase;
216 | buildActionMask = 2147483647;
217 | files = (
218 | );
219 | inputFileListPaths = (
220 | );
221 | inputPaths = (
222 | );
223 | name = "Start Packager";
224 | outputFileListPaths = (
225 | );
226 | outputPaths = (
227 | );
228 | runOnlyForDeploymentPostprocessing = 0;
229 | shellPath = /bin/sh;
230 | shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n";
231 | showEnvVarsInLog = 0;
232 | };
233 | /* End PBXShellScriptBuildPhase section */
234 |
235 | /* Begin PBXSourcesBuildPhase section */
236 | 13B07F871A680F5B00A75B9A /* Sources */ = {
237 | isa = PBXSourcesBuildPhase;
238 | buildActionMask = 2147483647;
239 | files = (
240 | 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
241 | 13B07FC11A68108700A75B9A /* main.m in Sources */,
242 | );
243 | runOnlyForDeploymentPostprocessing = 0;
244 | };
245 | /* End PBXSourcesBuildPhase section */
246 |
247 | /* Begin PBXVariantGroup section */
248 | 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
249 | isa = PBXVariantGroup;
250 | children = (
251 | 13B07FB21A68108700A75B9A /* Base */,
252 | );
253 | name = LaunchScreen.xib;
254 | path = PollsApp;
255 | sourceTree = "";
256 | };
257 | /* End PBXVariantGroup section */
258 |
259 | /* Begin XCBuildConfiguration section */
260 | 13B07F941A680F5B00A75B9A /* Debug */ = {
261 | isa = XCBuildConfiguration;
262 | baseConfigurationReference = 40262907E515AF868B91782D /* Pods-PollsApp.debug.xcconfig */;
263 | buildSettings = {
264 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
265 | CLANG_ENABLE_MODULES = YES;
266 | CURRENT_PROJECT_VERSION = 1;
267 | ENABLE_BITCODE = NO;
268 | GCC_PREPROCESSOR_DEFINITIONS = (
269 | "$(inherited)",
270 | "FB_SONARKIT_ENABLED=1",
271 | );
272 | INFOPLIST_FILE = PollsApp/Info.plist;
273 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
274 | OTHER_LDFLAGS = (
275 | "$(inherited)",
276 | "-ObjC",
277 | "-lc++",
278 | );
279 | PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
280 | PRODUCT_NAME = PollsApp;
281 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
282 | SWIFT_VERSION = 5.0;
283 | TARGETED_DEVICE_FAMILY = "1,2";
284 | VERSIONING_SYSTEM = "apple-generic";
285 | };
286 | name = Debug;
287 | };
288 | 13B07F951A680F5B00A75B9A /* Release */ = {
289 | isa = XCBuildConfiguration;
290 | baseConfigurationReference = 05E7432CC241106C9DDC61CB /* Pods-PollsApp.release.xcconfig */;
291 | buildSettings = {
292 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
293 | CLANG_ENABLE_MODULES = YES;
294 | CURRENT_PROJECT_VERSION = 1;
295 | INFOPLIST_FILE = PollsApp/Info.plist;
296 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
297 | OTHER_LDFLAGS = (
298 | "$(inherited)",
299 | "-ObjC",
300 | "-lc++",
301 | );
302 | PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
303 | PRODUCT_NAME = PollsApp;
304 | SWIFT_VERSION = 5.0;
305 | TARGETED_DEVICE_FAMILY = "1,2";
306 | VERSIONING_SYSTEM = "apple-generic";
307 | };
308 | name = Release;
309 | };
310 | 83CBBA201A601CBA00E9B192 /* Debug */ = {
311 | isa = XCBuildConfiguration;
312 | buildSettings = {
313 | ALWAYS_SEARCH_USER_PATHS = NO;
314 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
315 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
316 | CLANG_CXX_LIBRARY = "libc++";
317 | CLANG_ENABLE_MODULES = YES;
318 | CLANG_ENABLE_OBJC_ARC = YES;
319 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
320 | CLANG_WARN_BOOL_CONVERSION = YES;
321 | CLANG_WARN_COMMA = YES;
322 | CLANG_WARN_CONSTANT_CONVERSION = YES;
323 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
324 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
325 | CLANG_WARN_EMPTY_BODY = YES;
326 | CLANG_WARN_ENUM_CONVERSION = YES;
327 | CLANG_WARN_INFINITE_RECURSION = YES;
328 | CLANG_WARN_INT_CONVERSION = YES;
329 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
330 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
331 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
332 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
333 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
334 | CLANG_WARN_STRICT_PROTOTYPES = YES;
335 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
336 | CLANG_WARN_UNREACHABLE_CODE = YES;
337 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
338 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
339 | COPY_PHASE_STRIP = NO;
340 | ENABLE_STRICT_OBJC_MSGSEND = YES;
341 | ENABLE_TESTABILITY = YES;
342 | GCC_C_LANGUAGE_STANDARD = gnu99;
343 | GCC_DYNAMIC_NO_PIC = NO;
344 | GCC_NO_COMMON_BLOCKS = YES;
345 | GCC_OPTIMIZATION_LEVEL = 0;
346 | GCC_PREPROCESSOR_DEFINITIONS = (
347 | "DEBUG=1",
348 | "$(inherited)",
349 | );
350 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
351 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
352 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
353 | GCC_WARN_UNDECLARED_SELECTOR = YES;
354 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
355 | GCC_WARN_UNUSED_FUNCTION = YES;
356 | GCC_WARN_UNUSED_VARIABLE = YES;
357 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
358 | LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
359 | LIBRARY_SEARCH_PATHS = (
360 | "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
361 | "\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
362 | "\"$(inherited)\"",
363 | );
364 | MTL_ENABLE_DEBUG_INFO = YES;
365 | ONLY_ACTIVE_ARCH = YES;
366 | SDKROOT = iphoneos;
367 | };
368 | name = Debug;
369 | };
370 | 83CBBA211A601CBA00E9B192 /* Release */ = {
371 | isa = XCBuildConfiguration;
372 | buildSettings = {
373 | ALWAYS_SEARCH_USER_PATHS = NO;
374 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
375 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
376 | CLANG_CXX_LIBRARY = "libc++";
377 | CLANG_ENABLE_MODULES = YES;
378 | CLANG_ENABLE_OBJC_ARC = YES;
379 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
380 | CLANG_WARN_BOOL_CONVERSION = YES;
381 | CLANG_WARN_COMMA = YES;
382 | CLANG_WARN_CONSTANT_CONVERSION = YES;
383 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
384 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
385 | CLANG_WARN_EMPTY_BODY = YES;
386 | CLANG_WARN_ENUM_CONVERSION = YES;
387 | CLANG_WARN_INFINITE_RECURSION = YES;
388 | CLANG_WARN_INT_CONVERSION = YES;
389 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
390 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
391 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
392 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
393 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
394 | CLANG_WARN_STRICT_PROTOTYPES = YES;
395 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
396 | CLANG_WARN_UNREACHABLE_CODE = YES;
397 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
398 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
399 | COPY_PHASE_STRIP = YES;
400 | ENABLE_NS_ASSERTIONS = NO;
401 | ENABLE_STRICT_OBJC_MSGSEND = YES;
402 | GCC_C_LANGUAGE_STANDARD = gnu99;
403 | GCC_NO_COMMON_BLOCKS = YES;
404 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
405 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
406 | GCC_WARN_UNDECLARED_SELECTOR = YES;
407 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
408 | GCC_WARN_UNUSED_FUNCTION = YES;
409 | GCC_WARN_UNUSED_VARIABLE = YES;
410 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
411 | LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
412 | LIBRARY_SEARCH_PATHS = (
413 | "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
414 | "\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
415 | "\"$(inherited)\"",
416 | );
417 | MTL_ENABLE_DEBUG_INFO = NO;
418 | SDKROOT = iphoneos;
419 | VALIDATE_PRODUCT = YES;
420 | };
421 | name = Release;
422 | };
423 | /* End XCBuildConfiguration section */
424 |
425 | /* Begin XCConfigurationList section */
426 | 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "PollsApp" */ = {
427 | isa = XCConfigurationList;
428 | buildConfigurations = (
429 | 13B07F941A680F5B00A75B9A /* Debug */,
430 | 13B07F951A680F5B00A75B9A /* Release */,
431 | );
432 | defaultConfigurationIsVisible = 0;
433 | defaultConfigurationName = Release;
434 | };
435 | 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "PollsApp" */ = {
436 | isa = XCConfigurationList;
437 | buildConfigurations = (
438 | 83CBBA201A601CBA00E9B192 /* Debug */,
439 | 83CBBA211A601CBA00E9B192 /* Release */,
440 | );
441 | defaultConfigurationIsVisible = 0;
442 | defaultConfigurationName = Release;
443 | };
444 | /* End XCConfigurationList section */
445 | };
446 | rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
447 | }
448 |
--------------------------------------------------------------------------------