|
| 1 | +import React, { PureComponent } from 'react'; |
| 2 | +import PropTypes from 'prop-types'; |
| 3 | +import { StyleSheet, Animated, Easing } from 'react-native'; |
| 4 | +import { colors } from '../../../styles/common'; |
| 5 | +import Device from '../../../util/Device'; |
| 6 | + |
| 7 | +const styles = StyleSheet.create({ |
| 8 | + root: { |
| 9 | + backgroundColor: colors.white, |
| 10 | + minHeight: 200, |
| 11 | + borderTopLeftRadius: 20, |
| 12 | + borderTopRightRadius: 20, |
| 13 | + paddingBottom: Device.isIphoneX() ? 24 : 0 |
| 14 | + }, |
| 15 | + transactionEdit: { |
| 16 | + position: 'absolute', |
| 17 | + width: '100%', |
| 18 | + height: '100%' |
| 19 | + }, |
| 20 | + transactionReview: { |
| 21 | + paddingTop: 24 |
| 22 | + } |
| 23 | +}); |
| 24 | + |
| 25 | +/** |
| 26 | + * PureComponent that handles most of the animation/transition logic |
| 27 | + */ |
| 28 | +class AnimatedTransactionModal extends PureComponent { |
| 29 | + static propTypes = { |
| 30 | + /** |
| 31 | + * Changes the mode to 'review' |
| 32 | + */ |
| 33 | + review: PropTypes.func, |
| 34 | + /** |
| 35 | + * Called when a user changes modes |
| 36 | + */ |
| 37 | + onModeChange: PropTypes.func, |
| 38 | + /** |
| 39 | + * Whether or not basic gas estimates have been fetched |
| 40 | + */ |
| 41 | + ready: PropTypes.bool, |
| 42 | + /** |
| 43 | + * Children components |
| 44 | + */ |
| 45 | + children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired |
| 46 | + }; |
| 47 | + |
| 48 | + state = { |
| 49 | + originComponent: React.Children.toArray(this.props?.children).length > 1 ? 'dapp' : 'wallet', |
| 50 | + modalValue: |
| 51 | + React.Children.toArray(this.props?.children).length > 1 ? new Animated.Value(1) : new Animated.Value(0), |
| 52 | + width: Device.getDeviceWidth(), |
| 53 | + rootHeight: null, |
| 54 | + customGasHeight: null, |
| 55 | + transactionReviewDataHeight: null, |
| 56 | + hideGasSelectors: false, |
| 57 | + hideData: true, |
| 58 | + advancedCustomGas: false, |
| 59 | + toAdvancedFrom: 'edit', |
| 60 | + mode: 'review' |
| 61 | + }; |
| 62 | + |
| 63 | + reviewToEditValue = new Animated.Value(0); |
| 64 | + reviewToDataValue = new Animated.Value(0); |
| 65 | + editToAdvancedValue = new Animated.Value(0); |
| 66 | + |
| 67 | + xTranslationMappings = { |
| 68 | + reviewToEdit: this.reviewToEditValue, |
| 69 | + editToAdvanced: this.editToAdvancedValue, |
| 70 | + reviewToData: this.reviewToDataValue |
| 71 | + }; |
| 72 | + |
| 73 | + review = () => { |
| 74 | + this.props.review(); |
| 75 | + this.onModeChange('review'); |
| 76 | + }; |
| 77 | + |
| 78 | + onModeChange = mode => { |
| 79 | + if (mode === 'edit') { |
| 80 | + this.setState({ toAdvancedFrom: 'review' }); |
| 81 | + this.animate({ |
| 82 | + modalEndValue: this.state.advancedCustomGas ? this.getAnimatedModalValueForAdvancedCG() : 0, |
| 83 | + xTranslationName: 'reviewToEdit', |
| 84 | + xTranslationEndValue: 1 |
| 85 | + }); |
| 86 | + } else { |
| 87 | + this.animate({ |
| 88 | + modalEndValue: 1, |
| 89 | + xTranslationName: 'reviewToEdit', |
| 90 | + xTranslationEndValue: 0 |
| 91 | + }); |
| 92 | + } |
| 93 | + this.props.onModeChange(mode); |
| 94 | + }; |
| 95 | + |
| 96 | + animate = ({ modalEndValue, xTranslationName, xTranslationEndValue }) => { |
| 97 | + const { modalValue } = this.state; |
| 98 | + this.hideComponents(xTranslationName, xTranslationEndValue, 'start'); |
| 99 | + Animated.parallel([ |
| 100 | + Animated.timing(modalValue, { |
| 101 | + toValue: modalEndValue, |
| 102 | + duration: 250, |
| 103 | + easing: Easing.ease, |
| 104 | + useNativeDriver: true |
| 105 | + }), |
| 106 | + Animated.timing(this.xTranslationMappings[xTranslationName], { |
| 107 | + toValue: xTranslationEndValue, |
| 108 | + duration: 250, |
| 109 | + easing: Easing.ease, |
| 110 | + useNativeDriver: true |
| 111 | + }) |
| 112 | + ]).start(() => { |
| 113 | + this.hideComponents(xTranslationName, xTranslationEndValue, 'end'); |
| 114 | + }); |
| 115 | + }; |
| 116 | + |
| 117 | + toggleAdvancedCustomGas = (toggle = false) => { |
| 118 | + const { advancedCustomGas } = this.state; |
| 119 | + this.setState({ advancedCustomGas: toggle ? true : !advancedCustomGas, toAdvancedFrom: 'edit' }); |
| 120 | + }; |
| 121 | + |
| 122 | + hideComponents = (xTranslationName, xTranslationEndValue, animationTime) => { |
| 123 | + //data view is hidden by default because when we switch from review to edit, since view is nested in review, it also gets transformed. It's shown if it's the animation's destination. |
| 124 | + if (xTranslationName === 'editToAdvanced') { |
| 125 | + this.setState({ |
| 126 | + hideGasSelectors: xTranslationEndValue === 1 && animationTime === 'end' |
| 127 | + }); |
| 128 | + } |
| 129 | + if (xTranslationName === 'reviewToData') { |
| 130 | + this.setState({ |
| 131 | + hideData: xTranslationEndValue === 0 && animationTime === 'end' |
| 132 | + }); |
| 133 | + } |
| 134 | + }; |
| 135 | + |
| 136 | + generateTransform = (valueType, outRange) => { |
| 137 | + const { modalValue } = this.state; |
| 138 | + if (valueType === 'modal' || valueType === 'saveButton') { |
| 139 | + return { |
| 140 | + transform: [ |
| 141 | + { |
| 142 | + translateY: modalValue.interpolate({ |
| 143 | + inputRange: [0, valueType === 'saveButton' ? this.getAnimatedModalValueForAdvancedCG() : 1], |
| 144 | + outputRange: outRange |
| 145 | + }) |
| 146 | + } |
| 147 | + ] |
| 148 | + }; |
| 149 | + } |
| 150 | + let value; |
| 151 | + if (valueType === 'reviewToEdit') value = this.reviewToEditValue; |
| 152 | + else if (valueType === 'editToAdvanced') value = this.editToAdvancedValue; |
| 153 | + else if (valueType === 'reviewToData') value = this.reviewToDataValue; |
| 154 | + return { |
| 155 | + transform: [ |
| 156 | + { |
| 157 | + translateX: value.interpolate({ |
| 158 | + inputRange: [0, 1], |
| 159 | + outputRange: outRange |
| 160 | + }) |
| 161 | + } |
| 162 | + ] |
| 163 | + }; |
| 164 | + }; |
| 165 | + |
| 166 | + getAnimatedModalValueForAdvancedCG = () => { |
| 167 | + const { rootHeight, customGasHeight, originComponent } = this.state; |
| 168 | + if (originComponent === 'wallet') return 1; |
| 169 | + //70 is the fixed height + margin of the error message in advanced custom gas. It expands 70 units vertically to accomodate it |
| 170 | + return 70 / (rootHeight - customGasHeight); |
| 171 | + }; |
| 172 | + |
| 173 | + saveRootHeight = event => this.setState({ rootHeight: event.nativeEvent.layout.height }); |
| 174 | + |
| 175 | + saveCustomGasHeight = event => this.setState({ customGasHeight: event.nativeEvent.layout.height }); |
| 176 | + |
| 177 | + saveTransactionReviewDataHeight = event => |
| 178 | + !this.state.transactionReviewDataHeight && |
| 179 | + this.setState({ transactionReviewDataHeight: event.nativeEvent.layout.height }); |
| 180 | + |
| 181 | + getTransformValue = () => { |
| 182 | + const { rootHeight, customGasHeight } = this.state; |
| 183 | + return rootHeight - customGasHeight; |
| 184 | + }; |
| 185 | + |
| 186 | + render = () => { |
| 187 | + const { |
| 188 | + width, |
| 189 | + hideData, |
| 190 | + originComponent, |
| 191 | + customGasHeight, |
| 192 | + advancedCustomGas, |
| 193 | + hideGasSelectors, |
| 194 | + toAdvancedFrom |
| 195 | + } = this.state; |
| 196 | + const { ready, children } = this.props; |
| 197 | + const components = React.Children.toArray(children); |
| 198 | + let gasTransformStyle; |
| 199 | + let modalTransformStyle; |
| 200 | + let gasComponent; |
| 201 | + if (originComponent === 'dapp') { |
| 202 | + gasTransformStyle = this.generateTransform('reviewToEdit', [width, 0]); |
| 203 | + modalTransformStyle = this.generateTransform('modal', [this.getTransformValue(), 0]); |
| 204 | + gasComponent = components[1]; |
| 205 | + } else { |
| 206 | + gasTransformStyle = this.generateTransform('reviewToEdit', [0, -width]); |
| 207 | + modalTransformStyle = this.generateTransform('modal', [70, 0]); |
| 208 | + gasComponent = components[0]; |
| 209 | + } |
| 210 | + |
| 211 | + return ( |
| 212 | + <Animated.View |
| 213 | + style={[ |
| 214 | + styles.root, |
| 215 | + modalTransformStyle, |
| 216 | + originComponent === 'wallet' && { height: customGasHeight + 70 } |
| 217 | + ]} |
| 218 | + onLayout={this.saveRootHeight} |
| 219 | + > |
| 220 | + {originComponent === 'dapp' && ( |
| 221 | + <Animated.View |
| 222 | + style={[this.generateTransform('reviewToEdit', [0, -width]), styles.transactionReview]} |
| 223 | + > |
| 224 | + {React.cloneElement(components[0], { |
| 225 | + ...components[0].props, |
| 226 | + customGasHeight, |
| 227 | + hideData, |
| 228 | + generateTransform: this.generateTransform, |
| 229 | + animate: this.animate, |
| 230 | + saveTransactionReviewDataHeight: this.saveTransactionReviewDataHeight, |
| 231 | + onModeChange: this.onModeChange |
| 232 | + })} |
| 233 | + </Animated.View> |
| 234 | + )} |
| 235 | + |
| 236 | + {ready && ( |
| 237 | + <Animated.View style={[styles.transactionEdit, gasTransformStyle]}> |
| 238 | + {React.cloneElement(gasComponent, { |
| 239 | + ...gasComponent.props, |
| 240 | + advancedCustomGas, |
| 241 | + hideGasSelectors, |
| 242 | + toAdvancedFrom, |
| 243 | + onModeChange: this.onModeChange, |
| 244 | + toggleAdvancedCustomGas: this.toggleAdvancedCustomGas, |
| 245 | + saveCustomGasHeight: this.saveCustomGasHeight, |
| 246 | + animate: this.animate, |
| 247 | + generateTransform: this.generateTransform, |
| 248 | + getAnimatedModalValueForAdvancedCG: this.getAnimatedModalValueForAdvancedCG, |
| 249 | + review: this.review |
| 250 | + })} |
| 251 | + </Animated.View> |
| 252 | + )} |
| 253 | + </Animated.View> |
| 254 | + ); |
| 255 | + }; |
| 256 | +} |
| 257 | + |
| 258 | +export default AnimatedTransactionModal; |
0 commit comments