Página da disciplina de pos (Programação Orientada a Serviços) do curso técnico integrado de Informática para Internet.
Objetivos:
cd CurrencyConverter
yarn add redux-saga
yarn start
app/screens/Home.js :
verificar se o resultado da requisição a API já está completa (isFetching
)
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { KeyboardAvoidingView, StatusBar } from 'react-native';
import { connect } from 'react-redux';
import { Container } from '../components/Container';
import { Logo } from '../components/Logo';
import { InputWithButton } from '../components/TextInput';
import { ClearButton } from '../components/Button';
import { LastConverted } from '../components/Text';
import { Header } from '../components/Header';
import { changeCurrencyAmount, swapCurrency } from '../actions/currencies';
class Home extends Component {
static propTypes = {
navigation: PropTypes.object,
dispatch: PropTypes.func,
baseCurrency: PropTypes.string,
quoteCurrency: PropTypes.string,
amount: PropTypes.number,
conversionRate: PropTypes.number,
lastConvertedDate: PropTypes.object,
primaryColor: PropTypes.string,
isFetching: PropTypes.bool,
};
handleChangeText = (text) => {
this.props.dispatch(changeCurrencyAmount(text));
};
handlePressBaseCurrency = () => {
this.props.navigation.navigate('CurrencyList', { title: 'Base currency', type: 'base' });
};
handlePressQuoteCurrency = () => {
this.props.navigation.navigate('CurrencyList', { title: 'Quote currency', type: 'quote' });
};
handleSwapCurrency = () => {
this.props.dispatch(swapCurrency());
};
handleOptionsPress = () => {
this.props.navigation.navigate('Options');
};
render() {
let quotePrice = '...';
if (!this.props.isFetching) {
quotePrice = (this.props.amount * this.props.conversionRate).toFixed(2);
}
return (
<Container backgroundColor={this.props.primaryColor}>
<StatusBar backgroundColor="blue" barStyle="light-content" />
<Header onPress={this.handleOptionsPress} />
<KeyboardAvoidingView behavior="padding">
<Logo tintColor={this.props.primaryColor} />
<InputWithButton
buttonText={this.props.baseCurrency}
onPress={this.handlePressBaseCurrency}
defaultValue={this.props.amount.toString()}
keyboardType="numeric"
onChangeText={this.handleChangeText}
textColor={this.props.primaryColor}
/>
<InputWithButton
editable={false}
buttonText={this.props.quoteCurrency}
onPress={this.handlePressQuoteCurrency}
value={quotePrice}
textColor={this.props.primaryColor}
/>
<LastConverted
date={this.props.lastConvertedDate}
base={this.props.baseCurrency}
quote={this.props.quoteCurrency}
conversionRate={this.props.conversionRate}
/>
<ClearButton text="Reverse currencies" onPress={this.handleSwapCurrency} />
</KeyboardAvoidingView>
</Container>
);
}
}
const mapStateToProps = (state) => {
const { baseCurrency, quoteCurrency } = state.currencies;
const conversionSelector = state.currencies.conversions[baseCurrency] || {};
const rates = conversionSelector.rates || {};
return {
baseCurrency: state.currencies.baseCurrency,
quoteCurrency: state.currencies.quoteCurrency,
amount: state.currencies.amount,
conversionRate: rates[quoteCurrency] || 0,
lastConvertedDate: conversionSelector.date ? new Date(conversionSelector.date) : new Date(),
primaryColor: state.theme.primaryColor,
isFetching: conversionSelector.isFetching,
};
};
export default connect(mapStateToProps)(Home);
app/reducers/currencies.js
import {
CHANGE_CURRENCY_AMOUNT,
SWAP_CURRENCY,
CHANGE_BASE_CURRENCY,
CHANGE_QUOTE_CURRENCY,
} from '../actions/currencies';
const initialState = {
baseCurrency: 'USD',
quoteCurrency: 'GBP',
amount: 100,
conversions: {
USD: {
isFetching: false,
base: 'USD',
date: '2017-05-31',
rates: {
AUD: 1.3416,
BGN: 1.743,
BRL: 3.2515,
CAD: 1.3464,
CHF: 0.97104,
CNY: 6.813,
CZK: 23.547,
DKK: 6.6302,
GBP: 0.77858,
HKD: 7.7908,
HRK: 6.6068,
HUF: 273.77,
IDR: 13308,
ILS: 3.5431,
INR: 64.463,
JPY: 110.86,
KRW: 1118.4,
MXN: 18.765,
MYR: 4.281,
NOK: 8.4117,
NZD: 1.4071,
PHP: 49.77,
PLN: 3.7173,
RON: 4.0687,
RUB: 56.774,
SEK: 8.6942,
SGD: 1.3829,
THB: 34.07,
TRY: 3.5366,
ZAR: 13.133,
EUR: 0.89119,
},
},
},
};
const setConversions = (state, action) => {
let conversion = {
isFetching: true,
date: '',
rates: {},
};
if (state.conversions[action.currency]) {
conversion = state.conversions[action.currency];
}
return {
...state.conversions,
[action.currency]: conversion,
};
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case CHANGE_CURRENCY_AMOUNT:
return {
...state,
amount: action.amount || 0,
};
case SWAP_CURRENCY:
return {
...state,
baseCurrency: state.quoteCurrency,
quoteCurrency: state.baseCurrency,
};
case CHANGE_BASE_CURRENCY:
return {
...state,
baseCurrency: action.currency,
conversions: setConversions(state, action),
};
case CHANGE_QUOTE_CURRENCY:
return {
...state,
quoteCurrency: action.currency,
conversions: setConversions(state, action),
};
default:
return state;
}
};
export default reducer;
Etapa 1. Criar um middleware app/config/sagas.js
export default function* rootSaga() {
yield;
}
Etapa 2. Configurar o middleware no mecanismo de armazenamento app/config/store.js
import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import reducer from '../reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const middleware = [sagaMiddleware];
if (process.env.NODE_ENV === 'development') {
middleware.push(logger);
}
const store = createStore(reducer, applyMiddleware(...middleware));
sagaMiddleware.run(rootSaga);
export default store;
Etapa 3. Enumerar e programar os eventos que precisam de atualização de dados
3.1. app/config/sagas.js
// 1. Swap currency
// 2. Change base currency
// 3. Initial app load
export default function* rootSaga() {
yield;
}
3.2. app/actions/currencies.js
export const CHANGE_CURRENCY_AMOUNT = 'CHANGE_CURRENCY_AMOUNT';
export const SWAP_CURRENCY = 'SWAP_CURRENCY';
export const CHANGE_BASE_CURRENCY = 'CHANGE_BASE_CURRENCY';
export const CHANGE_QUOTE_CURRENCY = 'CHANGE_QUOTE_CURRENCY';
export const GET_INITIAL_CONVERSION = 'GET_INITIAL_CONVERSION';
export const changeCurrencyAmount = amount => ({
type: CHANGE_CURRENCY_AMOUNT,
amount: parseFloat(amount),
});
export const swapCurrency = () => ({
type: SWAP_CURRENCY,
});
export const changeBaseCurrency = currency => ({
type: CHANGE_BASE_CURRENCY,
currency,
});
export const changeQuoteCurrency = currency => ({
type: CHANGE_QUOTE_CURRENCY,
currency,
});
export const getInitialConversion = () => ({
type: GET_INITIAL_CONVERSION,
});
3.3. app/config/sagas.js
import { takeEvery } from 'redux-saga/effects';
// 1. Swap currency
// 2. Change base currency
// 3. Initial app load
import { SWAP_CURRENCY, CHANGE_BASE_CURRENCY, GET_INITIAL_CONVERSION } from '../actions/currencies';
function* fetchLatestConversionRates(action) {
console.log('TODO: Update the things', action);
yield;
}
function* rootSaga() {
yield takeEvery(GET_INITIAL_CONVERSION, fetchLatestConversionRates);
yield takeEvery(SWAP_CURRENCY, fetchLatestConversionRates);
yield takeEvery(CHANGE_BASE_CURRENCY, fetchLatestConversionRates);
}
export default rootSaga;
Etapa 3.4. app/screens/Home.js
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { KeyboardAvoidingView, StatusBar } from 'react-native';
import { connect } from 'react-redux';
import { Container } from '../components/Container';
import { Logo } from '../components/Logo';
import { InputWithButton } from '../components/TextInput';
import { ClearButton } from '../components/Button';
import { LastConverted } from '../components/Text';
import { Header } from '../components/Header';
import { changeCurrencyAmount, swapCurrency, getInitialConversion } from '../actions/currencies';
class Home extends Component {
static propTypes = {
navigation: PropTypes.object,
dispatch: PropTypes.func,
baseCurrency: PropTypes.string,
quoteCurrency: PropTypes.string,
amount: PropTypes.number,
conversionRate: PropTypes.number,
lastConvertedDate: PropTypes.object,
primaryColor: PropTypes.string,
isFetching: PropTypes.bool,
};
componentWillMount() {
this.props.dispatch(getInitialConversion());
}
handleChangeText = (text) => {
this.props.dispatch(changeCurrencyAmount(text));
};
handlePressBaseCurrency = () => {
this.props.navigation.navigate('CurrencyList', { title: 'Base currency', type: 'base' });
};
handlePressQuoteCurrency = () => {
this.props.navigation.navigate('CurrencyList', { title: 'Quote currency', type: 'quote' });
};
handleSwapCurrency = () => {
this.props.dispatch(swapCurrency());
};
handleOptionsPress = () => {
this.props.navigation.navigate('Options');
};
render() {
let quotePrice = '...';
if (!this.props.isFetching) {
quotePrice = (this.props.amount * this.props.conversionRate).toFixed(2);
}
return (
<Container backgroundColor={this.props.primaryColor}>
<StatusBar backgroundColor="blue" barStyle="light-content" />
<Header onPress={this.handleOptionsPress} />
<KeyboardAvoidingView behavior="padding">
<Logo tintColor={this.props.primaryColor} />
<InputWithButton
buttonText={this.props.baseCurrency}
onPress={this.handlePressBaseCurrency}
defaultValue={this.props.amount.toString()}
keyboardType="numeric"
onChangeText={this.handleChangeText}
textColor={this.props.primaryColor}
/>
<InputWithButton
editable={false}
buttonText={this.props.quoteCurrency}
onPress={this.handlePressQuoteCurrency}
value={quotePrice}
textColor={this.props.primaryColor}
/>
<LastConverted
date={this.props.lastConvertedDate}
base={this.props.baseCurrency}
quote={this.props.quoteCurrency}
conversionRate={this.props.conversionRate}
/>
<ClearButton text="Reverse currencies" onPress={this.handleSwapCurrency} />
</KeyboardAvoidingView>
</Container>
);
}
}
const mapStateToProps = (state) => {
const { baseCurrency, quoteCurrency } = state.currencies;
const conversionSelector = state.currencies.conversions[baseCurrency] || {};
const rates = conversionSelector.rates || {};
return {
baseCurrency: state.currencies.baseCurrency,
quoteCurrency: state.currencies.quoteCurrency,
amount: state.currencies.amount,
conversionRate: rates[quoteCurrency] || 0,
lastConvertedDate: conversionSelector.date ? new Date(conversionSelector.date) : new Date(),
primaryColor: state.theme.primaryColor,
isFetching: conversionSelector.isFetching,
};
};
export default connect(mapStateToProps)(Home);
Etapa 4. Programar acesso a API app/config/sagas.js
import { takeEvery, select, call } from 'redux-saga/effects';
import { SWAP_CURRENCY, CHANGE_BASE_CURRENCY, GET_INITIAL_CONVERSION } from '../actions/currencies';
const getLatestRate = currency => fetch(`https://frankfurter.app/current?from=${currency}`);
function* fetchLatestConversionRates(action) {
try {
let { currency } = action;
if (currency === undefined) {
currency = yield select(state => state.currencies.baseCurrency);
}
console.log('currency', currency);
const response = yield call(getLatestRate, currency);
const result = yield response.json();
console.log('result', result);
} catch (err) {
console.log('Saga error', err);
}
}
function* rootSaga() {
yield takeEvery(GET_INITIAL_CONVERSION, fetchLatestConversionRates);
yield takeEvery(SWAP_CURRENCY, fetchLatestConversionRates);
yield takeEvery(CHANGE_BASE_CURRENCY, fetchLatestConversionRates);
}
export default rootSaga;
Etapa 5. Programar a atualização dos dados
Etapa 5.1. app/actions/currencies.js : adicionar constantes das ações de acesso a API
export const CHANGE_CURRENCY_AMOUNT = 'CHANGE_CURRENCY_AMOUNT';
export const SWAP_CURRENCY = 'SWAP_CURRENCY';
export const CHANGE_BASE_CURRENCY = 'CHANGE_BASE_CURRENCY';
export const CHANGE_QUOTE_CURRENCY = 'CHANGE_QUOTE_CURRENCY';
export const GET_INITIAL_CONVERSION = 'GET_INITIAL_CONVERSION';
export const CONVERSION_RESULT = 'CONVERSION_RESULT';
export const CONVERSION_ERROR = 'CONVERSION_ERROR';
export const changeCurrencyAmount = amount => ({
type: CHANGE_CURRENCY_AMOUNT,
amount: parseFloat(amount),
});
export const swapCurrency = () => ({
type: SWAP_CURRENCY,
});
export const changeBaseCurrency = currency => ({
type: CHANGE_BASE_CURRENCY,
currency,
});
export const changeQuoteCurrency = currency => ({
type: CHANGE_QUOTE_CURRENCY,
currency,
});
export const getInitialConversion = () => ({
type: GET_INITIAL_CONVERSION,
});
Etapa 5.2. app/config/sagas.js : Adionar ações de acesso a API com sucesso e com erro
import { takeEvery, select, call, put } from 'redux-saga/effects';
import {
SWAP_CURRENCY,
CHANGE_BASE_CURRENCY,
GET_INITIAL_CONVERSION,
CONVERSION_RESULT,
CONVERSION_ERROR,
} from '../actions/currencies';
const getLatestRate = currency => fetch(`https://frankfurter.app/current?from=${currency}`);
function* fetchLatestConversionRates(action) {
try {
let { currency } = action;
if (currency === undefined) {
currency = yield select(state => state.currencies.baseCurrency);
}
console.log('currency', currency);
const response = yield call(getLatestRate, currency);
const result = yield response.json();
console.log('result', result);
if (result.error) {
yield put({ type: CONVERSION_ERROR, error: result.error });
} else {
yield put({ type: CONVERSION_RESULT, result });
}
} catch (err) {
yield put({ type: CONVERSION_ERROR, error: err.message });
}
}
function* rootSaga() {
yield takeEvery(GET_INITIAL_CONVERSION, fetchLatestConversionRates);
yield takeEvery(SWAP_CURRENCY, fetchLatestConversionRates);
yield takeEvery(CHANGE_BASE_CURRENCY, fetchLatestConversionRates);
}
export default rootSaga;
Etapa 5.3. app/reducers/currencies.js : programar o redutor para mapear dos dados coletados para os dados do app
import {
CHANGE_CURRENCY_AMOUNT,
SWAP_CURRENCY,
CHANGE_BASE_CURRENCY,
CHANGE_QUOTE_CURRENCY,
GET_INITIAL_CONVERSION,
CONVERSION_RESULT,
CONVERSION_ERROR,
} from '../actions/currencies';
const initialState = {
baseCurrency: 'USD',
quoteCurrency: 'GBP',
amount: 100,
conversions: {},
error: null,
};
const setConversions = (state, action) => {
let conversion = {
isFetching: true,
date: '',
rates: {},
};
if (state.conversions[action.currency]) {
conversion = state.conversions[action.currency];
}
return {
...state.conversions,
[action.currency]: conversion,
};
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case CHANGE_CURRENCY_AMOUNT:
return {
...state,
amount: action.amount || 0,
};
case SWAP_CURRENCY:
return {
...state,
baseCurrency: state.quoteCurrency,
quoteCurrency: state.baseCurrency,
};
case CHANGE_BASE_CURRENCY:
return {
...state,
baseCurrency: action.currency,
conversions: setConversions(state, action),
};
case CHANGE_QUOTE_CURRENCY:
return {
...state,
quoteCurrency: action.currency,
conversions: setConversions(state, action),
};
case GET_INITIAL_CONVERSION:
return {
...state,
conversions: setConversions(state, { currency: state.baseCurrency }),
};
case CONVERSION_RESULT:
return {
...state,
baseCurrency: action.result.base,
conversions: {
...state.conversions,
[action.result.base]: {
isFetching: false,
...action.result,
},
},
};
case CONVERSION_ERROR:
return {
...state,
error: action.error,
};
default:
return state;
}
};
export default reducer;
app/screens/Home.js
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { KeyboardAvoidingView, StatusBar } from 'react-native';
import { connect } from 'react-redux';
import { connectAlert } from '../components/Alert';
import { Container } from '../components/Container';
import { ClearButton } from '../components/Button';
import { Header } from '../components/Header';
import { InputWithButton } from '../components/TextInput';
import { LastConverted } from '../components/Text';
import { Logo } from '../components/Logo';
import { changeCurrencyAmount, swapCurrency, getInitialConversion } from '../actions/currencies';
class Home extends Component {
static propTypes = {
navigation: PropTypes.object,
dispatch: PropTypes.func,
baseCurrency: PropTypes.string,
quoteCurrency: PropTypes.string,
amount: PropTypes.number,
conversionRate: PropTypes.number,
lastConvertedDate: PropTypes.object,
primaryColor: PropTypes.string,
isFetching: PropTypes.bool,
currencyError: PropTypes.string,
alertWithType: PropTypes.func,
};
componentWillMount() {
this.props.dispatch(getInitialConversion());
}
componentWillReceiveProps(nextProps) {
if (nextProps.currencyError && !this.props.currencyError) {
this.props.alertWithType('error', 'Error', nextProps.currencyError);
}
}
handleChangeText = (text) => {
this.props.dispatch(changeCurrencyAmount(text));
};
handlePressBaseCurrency = () => {
this.props.navigation.navigate('CurrencyList', { title: 'Base currency', type: 'base' });
};
handlePressQuoteCurrency = () => {
this.props.navigation.navigate('CurrencyList', { title: 'Quote currency', type: 'quote' });
};
handleSwapCurrency = () => {
this.props.dispatch(swapCurrency());
};
handleOptionsPress = () => {
this.props.navigation.navigate('Options');
};
render() {
let quotePrice = '...';
if (!this.props.isFetching) {
quotePrice = (this.props.amount * this.props.conversionRate).toFixed(2);
}
return (
<Container backgroundColor={this.props.primaryColor}>
<StatusBar backgroundColor="blue" barStyle="light-content" />
<Header onPress={this.handleOptionsPress} />
<KeyboardAvoidingView behavior="padding">
<Logo tintColor={this.props.primaryColor} />
<InputWithButton
buttonText={this.props.baseCurrency}
onPress={this.handlePressBaseCurrency}
defaultValue={this.props.amount.toString()}
keyboardType="numeric"
onChangeText={this.handleChangeText}
textColor={this.props.primaryColor}
/>
<InputWithButton
editable={false}
buttonText={this.props.quoteCurrency}
onPress={this.handlePressQuoteCurrency}
value={quotePrice}
textColor={this.props.primaryColor}
/>
<LastConverted
date={this.props.lastConvertedDate}
base={this.props.baseCurrency}
quote={this.props.quoteCurrency}
conversionRate={this.props.conversionRate}
/>
<ClearButton text="Reverse currencies" onPress={this.handleSwapCurrency} />
</KeyboardAvoidingView>
</Container>
);
}
}
const mapStateToProps = (state) => {
const { baseCurrency, quoteCurrency } = state.currencies;
const conversionSelector = state.currencies.conversions[baseCurrency] || {};
const rates = conversionSelector.rates || {};
return {
baseCurrency: state.currencies.baseCurrency,
quoteCurrency: state.currencies.quoteCurrency,
amount: state.currencies.amount,
conversionRate: rates[quoteCurrency] || 0,
lastConvertedDate: conversionSelector.date ? new Date(conversionSelector.date) : new Date(),
primaryColor: state.theme.primaryColor,
isFetching: conversionSelector.isFetching,
currencyError: state.currencies.error,
};
};
export default connect(mapStateToProps)(connectAlert(Home));