implement phone verification (#274)
* implement phone verification * update permissions in buildozer.spec
92
app/package-lock.json
generated
|
@ -3360,6 +3360,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"fuse.js": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-2.6.2.tgz",
|
||||
"integrity": "sha1-1dmU/alvVDtaUd84tyzsnMYNneo="
|
||||
},
|
||||
"gauge": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz",
|
||||
|
@ -3431,6 +3436,11 @@
|
|||
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
|
||||
"integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ=="
|
||||
},
|
||||
"google-libphonenumber": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-2.0.19.tgz",
|
||||
"integrity": "sha512-kwtbruT+eyiof081cxT1tltMTxgTOq3CQhUoEYBROC+vNf+COPqzfKJtVnDvgXQe4SzfbnAYkP8KoSpbJBIlSg=="
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||
|
@ -3980,21 +3990,11 @@
|
|||
}
|
||||
},
|
||||
"lbryinc": {
|
||||
"version": "github:lbryio/lbryinc#678c5098e2099dd1560b2fefa2795f38ca3ce07b",
|
||||
"from": "github:lbryio/lbryinc",
|
||||
"version": "github:lbryio/lbryinc#7910b565d7edda16be1c9d291f296982261ba60a",
|
||||
"from": "github:lbryio/lbryinc#phone-verification",
|
||||
"requires": {
|
||||
"lbry-redux": "github:lbryio/lbry-redux#421321a78397251589e5a890f4caa95e79975e2b",
|
||||
"lbry-redux": "github:lbryio/lbry-redux#31f7afa8a37f5741dac01fc1ecdf153f3bed95dc",
|
||||
"reselect": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lbry-redux": {
|
||||
"version": "github:lbryio/lbry-redux#421321a78397251589e5a890f4caa95e79975e2b",
|
||||
"from": "github:lbryio/lbry-redux",
|
||||
"requires": {
|
||||
"proxy-polyfill": "0.1.6",
|
||||
"reselect": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lcid": {
|
||||
|
@ -4098,6 +4098,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||
},
|
||||
"lodash.toarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
|
||||
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
|
||||
},
|
||||
"lodash.unset": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.5.2.tgz",
|
||||
|
@ -4564,6 +4569,14 @@
|
|||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
||||
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
|
||||
},
|
||||
"node-emoji": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.8.1.tgz",
|
||||
"integrity": "sha512-+ktMAh1Jwas+TnGodfCfjUbJKoANqPaJFN0z0iqh41eqD8dvguNzcitVSBSVK1pidz0AqGbLKcoVuVLRVZ/aVg==",
|
||||
"requires": {
|
||||
"lodash.toarray": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
|
||||
|
@ -5200,6 +5213,44 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-native-country-picker-modal": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-country-picker-modal/-/react-native-country-picker-modal-0.6.2.tgz",
|
||||
"integrity": "sha1-upcRi+Q3O+DBHNUeRF5r1Eji8co=",
|
||||
"requires": {
|
||||
"fuse.js": "2.6.2",
|
||||
"lodash": "4.12.0",
|
||||
"node-emoji": "1.8.1",
|
||||
"prop-types": "15.6.0",
|
||||
"react-native-safe-area-view": "^0.7.0",
|
||||
"world-countries": "1.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "http://registry.npmjs.org/lodash/-/lodash-4.12.0.tgz",
|
||||
"integrity": "sha1-K9bcRqBA9Z5obJcu0h2T3FkFMlg="
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz",
|
||||
"integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=",
|
||||
"requires": {
|
||||
"fbjs": "^0.8.16",
|
||||
"loose-envify": "^1.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"react-native-safe-area-view": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.7.0.tgz",
|
||||
"integrity": "sha512-SjLdW/Th0WVMhyngH4O6yC21S+O4U4AAG3QxBr7fZ2ftgjXSpKbDHAhEpxBdFwei6HsnsC2h9oYMtPpaW9nfGg==",
|
||||
"requires": {
|
||||
"hoist-non-react-statics": "^2.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-native-dismiss-keyboard": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-dismiss-keyboard/-/react-native-dismiss-keyboard-1.0.0.tgz",
|
||||
|
@ -5263,6 +5314,16 @@
|
|||
"react-native-image-pan-zoom": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"react-native-phone-input": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-phone-input/-/react-native-phone-input-0.2.1.tgz",
|
||||
"integrity": "sha1-rGhSoeo32NWP+D3tUtGNe2MD5mc=",
|
||||
"requires": {
|
||||
"google-libphonenumber": "^2.0.9",
|
||||
"lodash": "^4.17.4",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"react-native-safe-area-view": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.9.0.tgz",
|
||||
|
@ -6837,6 +6898,11 @@
|
|||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
|
||||
},
|
||||
"world-countries": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/world-countries/-/world-countries-1.8.0.tgz",
|
||||
"integrity": "sha1-F/SOfoRwrFohNq1pON/GVvwry5U="
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
||||
|
|
|
@ -8,13 +8,15 @@
|
|||
"dependencies": {
|
||||
"base-64": "^0.1.0",
|
||||
"lbry-redux": "lbryio/lbry-redux",
|
||||
"lbryinc": "lbryio/lbryinc",
|
||||
"lbryinc": "lbryio/lbryinc#phone-verification",
|
||||
"moment": "^2.22.1",
|
||||
"react": "16.2.0",
|
||||
"react-native": "0.55.3",
|
||||
"react-native-country-picker-modal": "^0.6.2",
|
||||
"react-native-fast-image": "^5.0.3",
|
||||
"react-native-fetch-blob": "^0.10.8",
|
||||
"react-native-image-zoom-viewer": "^2.2.5",
|
||||
"react-native-phone-input": "^0.2.1",
|
||||
"react-native-vector-icons": "^5.0.0",
|
||||
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
|
||||
"react-navigation": "^2.12.1",
|
||||
|
|
|
@ -268,7 +268,7 @@ class AppWithNavigationState extends React.Component {
|
|||
}
|
||||
|
||||
if ('toast' === currentDisplayType) {
|
||||
ToastAndroid.show(message, ToastAndroid.SHORT);
|
||||
ToastAndroid.show(message, ToastAndroid.LONG);
|
||||
}
|
||||
|
||||
dispatch(doHideNotification());
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doNotify } from 'lbry-redux';
|
||||
import DeviceIdRewardSubcard from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
notify: data => dispatch(doNotify(data))
|
||||
});
|
||||
|
||||
export default connect(null, perform)(DeviceIdRewardSubcard);
|
|
@ -1,48 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AsyncStorage,
|
||||
NativeModules,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import Button from '../button';
|
||||
import Colors from '../../styles/colors';
|
||||
import Constants from '../../constants';
|
||||
import Link from '../link';
|
||||
import rewardStyle from '../../styles/reward';
|
||||
|
||||
class DeviceIdRewardSubcard extends React.PureComponent {
|
||||
onAllowAccessPressed = () => {
|
||||
if (!NativeModules.UtilityModule) {
|
||||
return notify({
|
||||
message: 'The device ID could not be obtained due to a missing module.',
|
||||
displayType: ['toast']
|
||||
});
|
||||
}
|
||||
|
||||
NativeModules.UtilityModule.requestPhoneStatePermission();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={rewardStyle.subcard}>
|
||||
<Text style={rewardStyle.subtitle}>Pending action: Device ID</Text>
|
||||
<Text style={[rewardStyle.bottomMarginMedium, rewardStyle.subcardText]}>
|
||||
The app requires the phone state permission in order to identify your device for reward eligibility.
|
||||
</Text>
|
||||
<Button
|
||||
style={rewardStyle.actionButton}
|
||||
text={"Allow Access"}
|
||||
onPress={this.onAllowAccessPressed}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default DeviceIdRewardSubcard;
|
|
@ -86,9 +86,10 @@ class EmailRewardSubcard extends React.PureComponent {
|
|||
value={this.state.email}
|
||||
onChangeText={text => this.handleChangeText(text)} />
|
||||
{!this.state.verifyStarted && <Button style={rewardStyle.actionButton}
|
||||
text={"Send Verification Email"}
|
||||
text={"Send verification email"}
|
||||
onPress={this.onSendVerificationPressed} />}
|
||||
{this.state.verifyStarted && emailNewPending && <ActivityIndicator size={"small"} color={Colors.LbryGreen} />}
|
||||
{this.state.verifyStarted && emailNewPending &&
|
||||
<ActivityIndicator size={"small"} color={Colors.LbryGreen} style={rewardStyle.loading} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
28
app/src/component/phoneNumberRewardSubcard/index.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doNotify } from 'lbry-redux';
|
||||
import {
|
||||
doUserPhoneNew,
|
||||
doUserPhoneVerify,
|
||||
selectPhoneNewErrorMessage,
|
||||
selectPhoneNewIsPending,
|
||||
selectPhoneToVerify,
|
||||
selectPhoneVerifyIsPending,
|
||||
selectPhoneVerifyErrorMessage
|
||||
} from 'lbryinc';
|
||||
import PhoneNumberRewardSubcard from './view';
|
||||
|
||||
const select = state => ({
|
||||
phoneVerifyErrorMessage: selectPhoneVerifyErrorMessage(state),
|
||||
phoneVerifyIsPending: selectPhoneVerifyIsPending(state),
|
||||
phone: selectPhoneToVerify(state),
|
||||
phoneNewErrorMessage: selectPhoneNewErrorMessage(state),
|
||||
phoneNewIsPending: selectPhoneNewIsPending(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
addUserPhone: (phone, country_code) => dispatch(doUserPhoneNew(phone, country_code)),
|
||||
verifyPhone: (verificationCode) => dispatch(doUserPhoneVerify(verificationCode)),
|
||||
notify: data => dispatch(doNotify(data)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(PhoneNumberRewardSubcard);
|
253
app/src/component/phoneNumberRewardSubcard/view.js
Normal file
|
@ -0,0 +1,253 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AsyncStorage,
|
||||
DeviceEventEmitter,
|
||||
NativeModules,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Button from '../button';
|
||||
import Colors from '../../styles/colors';
|
||||
import Constants from '../../constants';
|
||||
import CountryPicker from 'react-native-country-picker-modal';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import Link from '../link';
|
||||
import PhoneInput from 'react-native-phone-input';
|
||||
import rewardStyle from '../../styles/reward';
|
||||
|
||||
class PhoneNumberRewardSubcard extends React.PureComponent {
|
||||
phoneInput = null;
|
||||
|
||||
picker = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
canReceiveSms: false,
|
||||
cca2: 'US',
|
||||
codeVerifyStarted: false,
|
||||
codeVerifySuccessful: false,
|
||||
countryCode: null,
|
||||
newPhoneAdded: false,
|
||||
number: null,
|
||||
verificationCode: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
DeviceEventEmitter.addListener('onReceiveSmsPermissionGranted', this.receiveSmsPermissionGranted);
|
||||
DeviceEventEmitter.addListener('onVerificationCodeReceived', this.receiveVerificationCode);
|
||||
|
||||
const { phone } = this.props;
|
||||
if (phone && String(phone).trim().length() > 0) {
|
||||
this.setState({ newPhoneAdded: true });
|
||||
}
|
||||
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.canReceiveSms().then(canReceiveSms => this.setState({ canReceiveSms }));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
DeviceEventEmitter.removeListener('onReceiveSmsPermissionGranted', this.receiveSmsPermissionGranted);
|
||||
DeviceEventEmitter.removeListener('onVerificationCodeReceived', this.receiveVerificationCode);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
phoneVerifyIsPending,
|
||||
phoneVerifyErrorMessage,
|
||||
notify,
|
||||
phoneNewErrorMessage,
|
||||
phoneNewIsPending,
|
||||
onPhoneVerifySuccessful
|
||||
} = this.props;
|
||||
|
||||
if (!phoneNewIsPending && (phoneNewIsPending !== prevProps.phoneNewIsPending)) {
|
||||
if (phoneNewErrorMessage) {
|
||||
notify({ message: String(phoneNewErrorMessage), displayType: ['toast'] });
|
||||
} else {
|
||||
this.setState({ newPhoneAdded: true });
|
||||
}
|
||||
}
|
||||
if (!phoneVerifyIsPending && (phoneVerifyIsPending !== prevProps.phoneVerifyIsPending)) {
|
||||
if (phoneVerifyErrorMessage) {
|
||||
notify({ message: String(phoneVerifyErrorMessage), displayType: ['toast'] });
|
||||
this.setState({ codeVerifyStarted: false });
|
||||
} else {
|
||||
notify({ message: 'Your phone number was successfully verified.', displayType: ['toast'] });
|
||||
this.setState({ codeVerifySuccessful: true });
|
||||
if (onPhoneVerifySuccessful) {
|
||||
onPhoneVerifySuccessful();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
receiveSmsPermissionGranted = () => {
|
||||
this.setState({ canReceiveSms: true });
|
||||
}
|
||||
|
||||
receiveVerificationCode = (evt) => {
|
||||
if (!this.state.newPhoneAdded || this.state.codeVerifySuccessful) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { verifyPhone } = this.props;
|
||||
this.setState({ codeVerifyStarted: true });
|
||||
verifyPhone(evt.code);
|
||||
}
|
||||
|
||||
onAllowAccessPressed = () => {
|
||||
if (!NativeModules.UtilityModule) {
|
||||
return notify({
|
||||
message: 'The required permission could not be obtained due to a missing module.',
|
||||
displayType: ['toast']
|
||||
});
|
||||
}
|
||||
|
||||
NativeModules.UtilityModule.requestReceiveSmsPermission();
|
||||
}
|
||||
|
||||
onSendTextPressed = () => {
|
||||
const { addUserPhone, notify } = this.props;
|
||||
|
||||
if (!this.phoneInput.isValidNumber()) {
|
||||
return notify({
|
||||
message: 'Please provide a valid telephone number.',
|
||||
displayType: ['toast']
|
||||
});
|
||||
}
|
||||
|
||||
const countryCode = this.phoneInput.getCountryCode();
|
||||
const number = this.phoneInput.getValue().replace('+' + countryCode, '');
|
||||
this.setState({ countryCode, number });
|
||||
addUserPhone(number, countryCode);
|
||||
}
|
||||
|
||||
onVerifyPressed = () => {
|
||||
if (this.state.codeVerifyStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { verifyPhone } = this.props;
|
||||
this.setState({ codeVerifyStarted: true });
|
||||
verifyPhone(this.state.verificationCode);
|
||||
}
|
||||
|
||||
onPressFlag = () => {
|
||||
if (this.picker) {
|
||||
this.picker.openModal();
|
||||
}
|
||||
}
|
||||
|
||||
selectCountry(country) {
|
||||
this.phoneInput.selectCountry(country.cca2.toLowerCase());
|
||||
this.setState({ cca2: country.cca2 });
|
||||
}
|
||||
|
||||
handleChangeText = (text) => {
|
||||
this.setState({ verificationCode: text });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
phoneVerifyIsPending,
|
||||
phoneVerifyErrorMessage,
|
||||
phone,
|
||||
phoneErrorMessage,
|
||||
phoneNewIsPending
|
||||
} = this.props;
|
||||
|
||||
if (this.state.codeVerifySuccessful) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={rewardStyle.subcard}>
|
||||
<Text style={rewardStyle.subtitle}>Pending action: Verify Phone Number</Text>
|
||||
{!this.state.canReceiveSms &&
|
||||
<View style={rewardStyle.smsPermissionContainer}>
|
||||
<Text style={[rewardStyle.bottomMarginMedium, rewardStyle.subcardText]}>
|
||||
You can grant access to the receive SMS permission in order to verify phone number. Alternatively, you can enter the verification code manually.
|
||||
</Text>
|
||||
<Button
|
||||
style={rewardStyle.actionButton}
|
||||
text={"Allow access"}
|
||||
onPress={this.onAllowAccessPressed}
|
||||
/>
|
||||
</View>}
|
||||
<View style={rewardStyle.phoneVerificationContainer}>
|
||||
{!this.state.newPhoneAdded &&
|
||||
<View>
|
||||
<Text style={[rewardStyle.bottomMarginMedium, rewardStyle.subcardText]}>Please enter your phone number to continue.</Text>
|
||||
<PhoneInput
|
||||
ref={(ref) => { this.phoneInput = ref; }}
|
||||
style={StyleSheet.flatten(rewardStyle.phoneInput)}
|
||||
textProps={{ placeholder: '(phone number)' }}
|
||||
textStyle={StyleSheet.flatten(rewardStyle.phoneInputText)}
|
||||
onPressFlag={this.onPressFlag} />
|
||||
{!phoneNewIsPending &&
|
||||
<Button
|
||||
style={[rewardStyle.actionButton, rewardStyle.topMarginMedium]}
|
||||
text={"Send verification text"}
|
||||
onPress={this.onSendTextPressed} />}
|
||||
{phoneNewIsPending &&
|
||||
<ActivityIndicator
|
||||
style={[rewardStyle.loading, rewardStyle.topMarginMedium]}
|
||||
size="small"
|
||||
color={Colors.LbryGreen} />}
|
||||
</View>}
|
||||
{this.state.newPhoneAdded &&
|
||||
<View>
|
||||
{!phoneVerifyIsPending && !this.codeVerifyStarted &&
|
||||
<View>
|
||||
<Text style={[rewardStyle.bottomMarginSmall, rewardStyle.subcardText]}>
|
||||
Please enter the verification code.
|
||||
</Text>
|
||||
<TextInput
|
||||
style={rewardStyle.verificationCodeInput}
|
||||
keyboardType="numeric"
|
||||
placeholder="0000"
|
||||
underlineColorAndroid="transparent"
|
||||
value={this.state.verificationCode}
|
||||
onChangeText={text => this.handleChangeText(text)}
|
||||
/>
|
||||
<Button
|
||||
style={[rewardStyle.actionButton, rewardStyle.topMarginSmall ]}
|
||||
text={"Verify"}
|
||||
onPress={this.onVerifyPressed} />
|
||||
</View>
|
||||
}
|
||||
{phoneVerifyIsPending &&
|
||||
<View>
|
||||
<Text style={rewardStyle.subcardText}>Verifying your phone number...</Text>
|
||||
<ActivityIndicator
|
||||
color={Colors.LbryGreen}
|
||||
size="small"
|
||||
style={[rewardStyle.loading, rewardStyle.topMarginMedium]} />
|
||||
</View>}
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
|
||||
<CountryPicker
|
||||
ref={(picker) => { this.picker = picker; }}
|
||||
cca2={this.state.cca2}
|
||||
filterable={true}
|
||||
onChange={value => this.selectCountry(value)}
|
||||
showCallingCode={true}
|
||||
translation="eng">
|
||||
<View />
|
||||
</CountryPicker>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default PhoneNumberRewardSubcard;
|
|
@ -13,24 +13,14 @@ class RewardSummary extends React.Component {
|
|||
const { user } = this.props;
|
||||
let actionsLeft = 0;
|
||||
if (!user || !user.has_verified_email) {
|
||||
actionsLeft++;
|
||||
actionsLeft++;
|
||||
}
|
||||
|
||||
this.setState({ actionsLeft }, () => {
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.canAcquireDeviceId().then(canAcquire => {
|
||||
if (!canAcquire) {
|
||||
this.setState({ actionsLeft: this.state.actionsLeft + 1 });
|
||||
return;
|
||||
}
|
||||
}).catch(err => {
|
||||
this.setState({ actionsLeft: this.state.actionsLeft + 1 });
|
||||
});
|
||||
} else {
|
||||
// unable to retrieve device ID because the native module is not present.
|
||||
this.setState({ actionsLeft: this.state.actionsLeft + 1 });
|
||||
}
|
||||
});
|
||||
if (!user || !user.is_identity_verified) {
|
||||
actionsLeft++;
|
||||
}
|
||||
|
||||
this.setState({ actionsLeft });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
DeviceEventEmitter,
|
||||
ActivityIndicator,
|
||||
NativeModules,
|
||||
ScrollView,
|
||||
|
@ -11,7 +10,7 @@ import {
|
|||
import { doInstallNew } from 'lbryinc';
|
||||
import Colors from '../../styles/colors';
|
||||
import Link from '../../component/link';
|
||||
import DeviceIdRewardSubcard from '../../component/deviceIdRewardSubcard';
|
||||
import PhoneNumberRewardSubcard from '../../component/phoneNumberRewardSubcard';
|
||||
import EmailRewardSubcard from '../../component/emailRewardSubcard';
|
||||
import PageHeader from '../../component/pageHeader';
|
||||
import RewardCard from '../../component/rewardCard';
|
||||
|
@ -19,33 +18,21 @@ import rewardStyle from '../../styles/reward';
|
|||
|
||||
class RewardsPage extends React.PureComponent {
|
||||
state = {
|
||||
canAcquireDeviceId: false,
|
||||
isEmailVerified: false,
|
||||
isIdentityVerified: false,
|
||||
isRewardApproved: false,
|
||||
verifyRequestStarted: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
DeviceEventEmitter.addListener('onPhoneStatePermissionGranted', this.phoneStatePermissionGranted);
|
||||
|
||||
this.props.fetchRewards();
|
||||
|
||||
const { user } = this.props;
|
||||
this.setState({
|
||||
isEmailVerified: (user && user.primary_email && user.has_verified_email),
|
||||
isIdentityVerified: (user && user.is_identity_verified),
|
||||
isRewardApproved: (user && user.is_reward_approved)
|
||||
});
|
||||
|
||||
if (NativeModules.UtilityModule) {
|
||||
const util = NativeModules.UtilityModule;
|
||||
util.canAcquireDeviceId().then(canAcquireDeviceId => {
|
||||
this.setState({ canAcquireDeviceId });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
DeviceEventEmitter.removeListener('onPhoneStatePermissionGranted', this.phoneStatePermissionGranted);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
@ -67,13 +54,24 @@ class RewardsPage extends React.PureComponent {
|
|||
}
|
||||
|
||||
renderVerification() {
|
||||
if (!this.state.isRewardApproved) {
|
||||
if (!this.state.isEmailVerified || !this.state.isIdentityVerified) {
|
||||
return (
|
||||
<View style={[rewardStyle.card, rewardStyle.verification]}>
|
||||
<Text style={rewardStyle.title}>Humans Only</Text>
|
||||
<Text style={rewardStyle.text}>Rewards are for human beings only. You'll have to prove you're one of us before you can claim any rewards.</Text>
|
||||
{!this.state.canAcquireDeviceId && <DeviceIdRewardSubcard />}
|
||||
{!this.state.isEmailVerified && <EmailRewardSubcard />}
|
||||
{!this.state.isIdentityVerified && <PhoneNumberRewardSubcard />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.isEmailVerified && this.state.isIdentityVerified && !this.state.isRewardApproved) {
|
||||
return (
|
||||
<View style={[rewardStyle.card, rewardStyle.verification]}>
|
||||
<Text style={rewardStyle.title}>Manual Reward Verification</Text>
|
||||
<Text style={rewardStyle.text}>
|
||||
You need to be manually verified before you can start claiming rewards. Please request to be verified on the <Link style={rewardStyle.textLink} href="https://discordapp.com/invite/Z3bERWA" text="LBRY Discord server" />.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -81,28 +79,6 @@ class RewardsPage extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
phoneStatePermissionGranted = () => {
|
||||
const { install, notify } = this.props;
|
||||
if (NativeModules.UtilityModule) {
|
||||
const util = NativeModules.UtilityModule;
|
||||
|
||||
// Double-check just to be sure
|
||||
util.canAcquireDeviceId().then(canAcquireDeviceId => {
|
||||
this.setState({ canAcquireDeviceId });
|
||||
if (canAcquireDeviceId) {
|
||||
util.getDeviceId(false).then(deviceId => {
|
||||
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
|
||||
doInstallNew(`android-${appVersion}`, deviceId);
|
||||
});
|
||||
}).catch((error) => {
|
||||
notify({ message: error, displayType: ['toast'] });
|
||||
this.setState({ canAcquireDeviceId: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderUnclaimedRewards() {
|
||||
const { claimed, fetching, rewards, user } = this.props;
|
||||
|
||||
|
@ -123,7 +99,8 @@ class RewardsPage extends React.PureComponent {
|
|||
return (
|
||||
<View style={rewardStyle.busyContainer}>
|
||||
<Text style={rewardStyle.infoText}>
|
||||
{(claimed && claimed.length) ? "You have claimed all available rewards! We're regularly adding more so be sure to check back later." :
|
||||
{(claimed && claimed.length) ?
|
||||
"You have claimed all available rewards! We're regularly adding more so be sure to check back later." :
|
||||
"There are no rewards available at this time, please check back later."}
|
||||
</Text>
|
||||
</View>
|
||||
|
@ -157,13 +134,11 @@ class RewardsPage extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<View style={rewardStyle.container}>
|
||||
{this.renderVerification()}
|
||||
<View style={rewardStyle.rewardsContainer}>
|
||||
<ScrollView style={rewardStyle.scrollContainer} contentContainerStyle={rewardStyle.scrollContentContainer}>
|
||||
{this.renderUnclaimedRewards()}
|
||||
{this.renderClaimedRewards()}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<ScrollView style={rewardStyle.scrollContainer} contentContainerStyle={rewardStyle.scrollContentContainer}>
|
||||
{this.renderVerification()}
|
||||
{this.renderUnclaimedRewards()}
|
||||
{this.renderClaimedRewards()}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -82,6 +82,12 @@ const rewardStyle = StyleSheet.create({
|
|||
marginTop: 2,
|
||||
marginBottom: 2
|
||||
},
|
||||
topMarginSmall: {
|
||||
marginTop: 8
|
||||
},
|
||||
topMarginMedium: {
|
||||
marginTop: 16
|
||||
},
|
||||
bottomMarginSmall: {
|
||||
marginBottom: 8
|
||||
},
|
||||
|
@ -96,6 +102,9 @@ const rewardStyle = StyleSheet.create({
|
|||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14,
|
||||
},
|
||||
textLink: {
|
||||
color: Colors.LbryGreen
|
||||
},
|
||||
leftCol: {
|
||||
width: '15%',
|
||||
alignItems: 'center',
|
||||
|
@ -153,6 +162,35 @@ const rewardStyle = StyleSheet.create({
|
|||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14,
|
||||
lineHeight: 22
|
||||
},
|
||||
phoneVerificationContainer: {
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4
|
||||
},
|
||||
phoneInput: {
|
||||
marginLeft: 8
|
||||
},
|
||||
phoneInputText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16,
|
||||
letterSpacing: 1.3
|
||||
},
|
||||
verifyingText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14,
|
||||
marginLeft: 12,
|
||||
alignSelf: 'flex-start'
|
||||
},
|
||||
verificationCodeInput: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 24,
|
||||
letterSpacing: 12
|
||||
},
|
||||
loading: {
|
||||
alignSelf: 'flex-start'
|
||||
},
|
||||
smsPermissionContainer: {
|
||||
marginBottom: 32
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ fullscreen = 0
|
|||
#android.presplash_color = #FFFFFF
|
||||
|
||||
# (list) Permissions
|
||||
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,READ_PHONE_STATE,WRITE_EXTERNAL_STORAGE
|
||||
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,RECEIVE_SMS,WRITE_EXTERNAL_STORAGE
|
||||
|
||||
# (int) Android API to use
|
||||
android.api = 27
|
||||
|
|
|
@ -86,7 +86,7 @@ fullscreen = 0
|
|||
#android.presplash_color = #FFFFFF
|
||||
|
||||
# (list) Permissions
|
||||
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,READ_PHONE_STATE,WRITE_EXTERNAL_STORAGE
|
||||
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,RECEIVE_SMS,WRITE_EXTERNAL_STORAGE
|
||||
|
||||
# (int) Android API to use
|
||||
android.api = 27
|
||||
|
|
|
@ -86,7 +86,7 @@ fullscreen = 0
|
|||
#android.presplash_color = #FFFFFF
|
||||
|
||||
# (list) Permissions
|
||||
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,READ_PHONE_STATE,WRITE_EXTERNAL_STORAGE
|
||||
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,RECEIVE_SMS,WRITE_EXTERNAL_STORAGE
|
||||
|
||||
# (int) Android API to use
|
||||
android.api = 27
|
||||
|
|
|
@ -8,10 +8,10 @@ jarsigner -verbose -sigalg SHA1withRSA \
|
|||
-digestalg SHA1 \
|
||||
-keystore lbry-android.keystore \
|
||||
-storepass $KEYSTORE_PASSWORD \
|
||||
bin/browser-$version-release-unsigned.apk lbry-android \
|
||||
bin/browser-$version-release-unsigned.apk lbry-android > /dev/null \
|
||||
&& mv bin/browser-$version-release-unsigned.apk bin/browser-$version-release-signed.apk
|
||||
~/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1/zipalign -v 4 \
|
||||
bin/browser-$version-release-signed.apk bin/browser-$version-release.apk \
|
||||
bin/browser-$version-release-signed.apk bin/browser-$version-release.apk > /dev/null \
|
||||
&& rm bin/browser-$version-release-signed.apk
|
||||
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import android.provider.Settings;
|
|||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.telephony.SmsMessage;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
@ -25,7 +26,9 @@ import com.facebook.react.common.LifecycleState;
|
|||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.RNFetchBlob.RNFetchBlobPackage;
|
||||
|
@ -52,10 +55,14 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
|
||||
private static final int PHONE_STATE_PERMISSION_REQ_CODE = 202;
|
||||
|
||||
private BroadcastReceiver stopServiceReceiver;
|
||||
private static final int RECEIVE_SMS_PERMISSION_REQ_CODE = 203;
|
||||
|
||||
private BroadcastReceiver backgroundMediaReceiver;
|
||||
|
||||
private BroadcastReceiver smsReceiver;
|
||||
|
||||
private BroadcastReceiver stopServiceReceiver;
|
||||
|
||||
private ReactRootView mReactRootView;
|
||||
|
||||
private ReactInstanceManager mReactInstanceManager;
|
||||
|
@ -96,6 +103,9 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
// Register the stop service receiver (so that we close the activity if the user requests the service to stop)
|
||||
registerStopReceiver();
|
||||
|
||||
// Register SMS receiver for handling verification texts
|
||||
registerSmsReceiver();
|
||||
|
||||
// Start the daemon service if it is not started
|
||||
serviceRunning = isServiceRunning(LbrynetService.class);
|
||||
if (!serviceRunning) {
|
||||
|
@ -159,6 +169,48 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
registerReceiver(backgroundMediaReceiver, backgroundMediaFilter);
|
||||
}
|
||||
|
||||
public void registerSmsReceiver() {
|
||||
if (!hasPermission(Manifest.permission.RECEIVE_SMS, this)) {
|
||||
// don't create the receiver if we don't have the read sms permission
|
||||
return;
|
||||
}
|
||||
|
||||
IntentFilter smsFilter = new IntentFilter();
|
||||
smsFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
|
||||
smsReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// Get the message
|
||||
Bundle bundle = intent.getExtras();
|
||||
if (bundle != null) {
|
||||
Object[] pdus = (Object[]) bundle.get("pdus");
|
||||
if (pdus != null && pdus.length > 0) {
|
||||
SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdus[0]);
|
||||
String text = sms.getMessageBody();
|
||||
if (text == null || text.trim().length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve verification code from the text message if it contains
|
||||
// the strings "lbry", "verification code" and the colon (following the expected format)
|
||||
text = text.toLowerCase();
|
||||
if (text.indexOf("lbry") > -1 && text.indexOf("verification code") > -1 && text.indexOf(":") > -1) {
|
||||
String code = text.substring(text.lastIndexOf(":") + 1).trim();
|
||||
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext != null) {
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putString("code", code);
|
||||
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("onVerificationCodeReceived", params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
registerReceiver(smsReceiver, smsFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
|
||||
|
@ -212,6 +264,27 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
"No permission granted to read your device state. Rewards cannot be claimed.", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
break;
|
||||
|
||||
case RECEIVE_SMS_PERMISSION_REQ_CODE:
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// Permission granted. Emit an onPhoneStatePermissionGranted event
|
||||
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext != null) {
|
||||
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("onReceiveSmsPermissionGranted", null);
|
||||
}
|
||||
|
||||
// register the receiver
|
||||
if (smsReceiver == null) {
|
||||
registerSmsReceiver();
|
||||
}
|
||||
} else {
|
||||
// Permission not granted. Simply show a message.
|
||||
Toast.makeText(this,
|
||||
"No permission granted to receive your SMS messages. You may have to enter the verification code manually.",
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,6 +365,11 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
backgroundMediaReceiver = null;
|
||||
}
|
||||
|
||||
if (smsReceiver != null) {
|
||||
unregisterReceiver(smsReceiver);
|
||||
smsReceiver = null;
|
||||
}
|
||||
|
||||
if (stopServiceReceiver != null) {
|
||||
unregisterReceiver(stopServiceReceiver);
|
||||
stopServiceReceiver = null;
|
||||
|
@ -358,6 +436,15 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
true);
|
||||
}
|
||||
|
||||
public static void checkReceiveSmsPermission(Context context) {
|
||||
// Request read phone state permission
|
||||
checkPermission(Manifest.permission.RECEIVE_SMS,
|
||||
RECEIVE_SMS_PERMISSION_REQ_CODE,
|
||||
"LBRY requires access to be able to read a verification text message for rewards.",
|
||||
context,
|
||||
true);
|
||||
}
|
||||
|
||||
private boolean isServiceRunning(Class<?> serviceClass) {
|
||||
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||
for (ActivityManager.RunningServiceInfo serviceInfo : manager.getRunningServices(Integer.MAX_VALUE)) {
|
||||
|
|
|
@ -109,9 +109,9 @@ public class UtilityModule extends ReactContextBaseJavaModule {
|
|||
}
|
||||
} catch (SecurityException ex) {
|
||||
// Maybe the permission was not granted? Try to acquire permission
|
||||
if (requestPermission) {
|
||||
/*if (requestPermission) {
|
||||
requestPhoneStatePermission();
|
||||
}
|
||||
}*/
|
||||
} catch (Exception ex) {
|
||||
// id could not be obtained. Display a warning that rewards cannot be claimed.
|
||||
promise.reject(ex.getMessage());
|
||||
|
@ -126,20 +126,16 @@ public class UtilityModule extends ReactContextBaseJavaModule {
|
|||
}
|
||||
|
||||
@ReactMethod
|
||||
public void canAcquireDeviceId(final Promise promise) {
|
||||
if (isEmulator()) {
|
||||
promise.resolve(false);
|
||||
}
|
||||
|
||||
promise.resolve(MainActivity.hasPermission(Manifest.permission.READ_PHONE_STATE, MainActivity.getActivity()));
|
||||
public void canReceiveSms(final Promise promise) {
|
||||
promise.resolve(MainActivity.hasPermission(Manifest.permission.RECEIVE_SMS, MainActivity.getActivity()));
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void requestPhoneStatePermission() {
|
||||
public void requestReceiveSmsPermission() {
|
||||
MainActivity activity = (MainActivity) MainActivity.getActivity();
|
||||
if (activity != null) {
|
||||
// Request for the READ_PHONE_STATE permission
|
||||
MainActivity.checkPhoneStatePermission(activity);
|
||||
// Request for the RECEIVE_SMS permission
|
||||
MainActivity.checkReceiveSmsPermission(activity);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 494 B |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 451 B |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 225 B |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 683 B |
After Width: | Height: | Size: 2 KiB |
After Width: | Height: | Size: 247 B |
After Width: | Height: | Size: 879 B |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 332 B |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 265 B |
After Width: | Height: | Size: 437 B |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 523 B |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 370 B |
After Width: | Height: | Size: 540 B |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 560 B |
After Width: | Height: | Size: 261 B |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 247 B |
After Width: | Height: | Size: 7 KiB |
After Width: | Height: | Size: 645 B |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 7.2 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 421 B |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 481 B |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 540 B |
After Width: | Height: | Size: 610 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 588 B |