1import * as React from 'react' 2import * as Kb from '../../../common-adapters' 3import * as Styles from '../../../styles' 4import {ParticipantsRow} from '../../common' 5import {isLargeScreen} from '../../../constants/platform' 6import {SelectedEntry, DropdownEntry, DropdownText} from './dropdown' 7import Search from './search' 8import {Account} from '.' 9import debounce from 'lodash/debounce' 10import defer from 'lodash/defer' 11 12export type ToKeybaseUserProps = { 13 isRequest: boolean 14 recipientUsername: string 15 errorMessage?: string 16 onShowProfile: (username: string) => void 17 onRemoveProfile: () => void 18 onChangeRecipient: (recipient: string) => void 19 onScanQRCode: (() => void) | null 20 onSearch: () => void 21} 22 23const placeholderExample = isLargeScreen ? 'Ex: G12345... or you*example.com' : 'G12.. or you*example.com' 24 25const ToKeybaseUser = (props: ToKeybaseUserProps) => { 26 if (props.recipientUsername) { 27 // A username has been set, so display their name and avatar. 28 return ( 29 <ParticipantsRow 30 heading={props.isRequest ? 'From' : 'To'} 31 headingAlignment="Left" 32 dividerColor={props.errorMessage ? Styles.globalColors.red : ''} 33 style={styles.toKeybaseUser} 34 > 35 <Kb.Box2 direction="vertical" fullWidth={true} style={styles.inputBox}> 36 <Kb.Box2 direction="horizontal" centerChildren={true} fullWidth={true}> 37 <Kb.ConnectedNameWithIcon 38 colorFollowing={true} 39 horizontal={true} 40 containerStyle={styles.toKeybaseUserNameWithIcon} 41 username={props.recipientUsername} 42 avatarStyle={styles.avatar} 43 avatarSize={32} 44 onClick="tracker" 45 /> 46 <Kb.Icon 47 type="iconfont-remove" 48 boxStyle={styles.keybaseUserRemoveButton} 49 fontSize={16} 50 color={Styles.globalColors.black_20} 51 onClick={props.onRemoveProfile} 52 /> 53 </Kb.Box2> 54 {!!props.errorMessage && ( 55 <Kb.Text type="BodySmall" style={styles.errorText}> 56 {props.errorMessage} 57 </Kb.Text> 58 )} 59 </Kb.Box2> 60 </ParticipantsRow> 61 ) 62 } 63 64 // No username, so show search box. 65 return ( 66 <Search 67 heading={props.isRequest ? 'From' : 'To'} 68 onClickResult={props.onChangeRecipient} 69 onSearch={props.onSearch} 70 onShowTracker={props.onShowProfile} 71 onScanQRCode={props.onScanQRCode} 72 /> 73 ) 74} 75 76export type ToStellarPublicKeyProps = { 77 recipientPublicKey: string 78 errorMessage?: string 79 onChangeRecipient: (recipient: string) => void 80 onScanQRCode: (() => void) | null 81 setReadyToReview: (ready: boolean) => void 82} 83 84const ToStellarPublicKey = (props: ToStellarPublicKeyProps) => { 85 const [recipientPublicKey, setRecipentPublicKey] = React.useState(props.recipientPublicKey) 86 const debouncedOnChangeRecip = React.useCallback(debounce(props.onChangeRecipient, 1e3), [ 87 props.onChangeRecipient, 88 ]) 89 90 const {setReadyToReview} = props 91 const onChangeRecipient = React.useCallback( 92 (recipientPublicKey: string) => { 93 setRecipentPublicKey(recipientPublicKey) 94 setReadyToReview(false) 95 debouncedOnChangeRecip(recipientPublicKey) 96 }, 97 [setReadyToReview, debouncedOnChangeRecip] 98 ) 99 100 React.useEffect(() => { 101 if (props.recipientPublicKey !== recipientPublicKey) { 102 // Hot fix to let any empty string textChange callbacks happen before we change the value. 103 defer(() => setRecipentPublicKey(props.recipientPublicKey)) 104 } 105 // We do not want this be called when the state changes 106 // Only when the prop.recipientPublicKey changes. 107 // eslint-disable-next-line react-hooks/exhaustive-deps 108 }, [props.recipientPublicKey]) 109 110 return ( 111 <ParticipantsRow 112 heading="To" 113 headingAlignment="Left" 114 headingStyle={styles.heading} 115 dividerColor={props.errorMessage ? Styles.globalColors.red : ''} 116 style={styles.toStellarPublicKey} 117 > 118 <Kb.Box2 direction="vertical" fullWidth={!Styles.isMobile} style={styles.inputBox}> 119 <Kb.Box2 direction="horizontal" gap="xxtiny" fullWidth={!Styles.isMobile} style={styles.inputInner}> 120 <Kb.Icon 121 sizeType={Styles.isMobile ? 'Small' : 'Default'} 122 type="iconfont-identity-stellar" 123 color={ 124 recipientPublicKey.length === 0 || props.errorMessage 125 ? Styles.globalColors.black_20 126 : Styles.globalColors.black 127 } 128 /> 129 <Kb.Box2 direction="horizontal" style={styles.publicKeyInputContainer}> 130 <Kb.NewInput 131 type="text" 132 onChangeText={onChangeRecipient} 133 textType="BodySemibold" 134 hideBorder={true} 135 containerStyle={styles.input} 136 multiline={true} 137 rowsMin={2} 138 rowsMax={3} 139 value={recipientPublicKey} 140 /> 141 {!recipientPublicKey && ( 142 <Kb.Box 143 activeOpacity={1} 144 pointerEvents="none" 145 style={Styles.collapseStyles([Styles.globalStyles.fillAbsolute, styles.placeholderContainer])} 146 > 147 <Kb.Text type="BodySemibold" style={styles.colorBlack20}> 148 Stellar address 149 </Kb.Text> 150 <Kb.Text type="BodySemibold" style={styles.colorBlack20} lineClamp={1} ellipsizeMode="middle"> 151 {placeholderExample} 152 </Kb.Text> 153 </Kb.Box> 154 )} 155 </Kb.Box2> 156 {!recipientPublicKey && props.onScanQRCode && ( 157 <Kb.Icon 158 color={Styles.globalColors.black_50} 159 type="iconfont-qr-code" 160 onClick={props.onScanQRCode} 161 style={styles.qrCode} 162 /> 163 )} 164 </Kb.Box2> 165 {!!props.errorMessage && ( 166 <Kb.Text type="BodySmall" style={styles.errorText}> 167 {props.errorMessage} 168 </Kb.Text> 169 )} 170 </Kb.Box2> 171 </ParticipantsRow> 172 ) 173} 174 175export type ToOtherAccountProps = { 176 user: string 177 toAccount?: Account 178 allAccounts: Account[] 179 onChangeRecipient: (recipient: string) => void 180 onLinkAccount: () => void 181 onCreateNewAccount: () => void 182 showSpinner: boolean 183} 184 185class ToOtherAccount extends React.Component<ToOtherAccountProps> { 186 onAccountDropdownChange = (node: React.ReactNode) => { 187 if (React.isValidElement(node)) { 188 const element: React.ReactElement = node 189 if (element.key === 'create-new') { 190 this.props.onCreateNewAccount() 191 } else if (element.key === 'link-existing') { 192 this.props.onLinkAccount() 193 } else { 194 this.props.onChangeRecipient(element.props.account.id) 195 } 196 } 197 } 198 199 render() { 200 if (this.props.allAccounts.length <= 1) { 201 // A user is sending to another account, but has no other 202 // accounts. Show a "create new account" button. 203 return ( 204 <ParticipantsRow heading="To" headingAlignment="Right" style={styles.toAccountRow}> 205 <Kb.Button 206 small={true} 207 type="Wallet" 208 style={styles.createNewAccountButton} 209 label="Create a new account" 210 onClick={this.props.onCreateNewAccount} 211 /> 212 </ParticipantsRow> 213 ) 214 } 215 216 // A user is sending from an account to another account with other 217 // accounts. Show a dropdown list of other accounts, in addition 218 // to the link existing and create new actions. 219 let items = [ 220 <DropdownText 221 spinner={this.props.showSpinner} 222 key="link-existing" 223 text="Link an existing Stellar account" 224 />, 225 <DropdownText spinner={this.props.showSpinner} key="create-new" text="Create a new account" />, 226 ] 227 228 if (this.props.allAccounts.length > 0) { 229 const walletItems = this.props.allAccounts.map(account => ( 230 <DropdownEntry key={account.id} account={account} user={this.props.user} /> 231 )) 232 items = walletItems.concat(items) 233 } 234 235 return ( 236 <ParticipantsRow heading="To" headingAlignment="Right" style={styles.toAccountRow}> 237 <Kb.Dropdown 238 onChanged={this.onAccountDropdownChange} 239 items={items} 240 style={styles.dropdown} 241 selectedBoxStyle={styles.dropdownSelectedBox} 242 selected={ 243 this.props.toAccount ? ( 244 <SelectedEntry 245 spinner={this.props.showSpinner} 246 account={this.props.toAccount} 247 user={this.props.user} 248 /> 249 ) : ( 250 <DropdownText 251 spinner={this.props.showSpinner} 252 key="placeholder-select" 253 text="Pick another account" 254 /> 255 ) 256 } 257 /> 258 </ParticipantsRow> 259 ) 260 } 261} 262 263const styles = Styles.styleSheetCreate( 264 () => 265 ({ 266 avatar: { 267 marginRight: 8, 268 }, 269 colorBlack20: { 270 color: Styles.globalColors.black_20, 271 }, 272 createNewAccountButton: Styles.platformStyles({ 273 isElectron: { 274 width: 194, 275 }, 276 }), 277 dropdown: Styles.platformStyles({ 278 isMobile: {height: 32}, 279 }), 280 dropdownSelectedBox: Styles.platformStyles({ 281 isMobile: {minHeight: 32}, 282 }), 283 errorText: Styles.platformStyles({ 284 common: { 285 color: Styles.globalColors.redDark, 286 width: '100%', 287 }, 288 isElectron: { 289 wordWrap: 'break-word', 290 }, 291 }), 292 heading: { 293 alignSelf: 'flex-start', 294 }, 295 input: Styles.platformStyles({ 296 common: { 297 padding: 0, 298 }, 299 isMobile: { 300 paddingLeft: Styles.globalMargins.xtiny, 301 }, 302 }), 303 inputBox: Styles.platformStyles({isElectron: {flexGrow: 1}, isMobile: {flex: 1}}), 304 inputInner: Styles.platformStyles({ 305 common: { 306 alignItems: 'flex-start', 307 flex: 1, 308 position: 'relative', 309 }, 310 isElectron: { 311 flexShrink: 0, 312 }, 313 }), 314 keybaseUserRemoveButton: { 315 flex: 1, 316 marginRight: Styles.globalMargins.tiny, 317 textAlign: 'right', // consistent with UserInput 318 }, 319 placeholderContainer: Styles.platformStyles({ 320 common: { 321 display: 'flex', 322 flexDirection: 'column', 323 paddingLeft: (Styles.isMobile ? 0 : 16) + 4, 324 }, 325 isElectron: { 326 pointerEvents: 'none', 327 }, 328 }), 329 publicKeyInputContainer: {flexGrow: 1, flexShrink: 1}, 330 qrCode: { 331 marginRight: Styles.globalMargins.tiny, 332 marginTop: Styles.globalMargins.tiny, 333 }, 334 toAccountRow: Styles.platformStyles({ 335 isMobile: { 336 height: 40, 337 paddingBottom: 4, 338 paddingTop: 4, 339 }, 340 }), 341 toKeybaseUser: { 342 height: 48, 343 }, 344 toKeybaseUserNameWithIcon: { 345 flexGrow: 1, 346 }, 347 toStellarPublicKey: { 348 alignItems: 'flex-start', 349 minHeight: 52, 350 }, 351 } as const) 352) 353 354export {ToKeybaseUser, ToStellarPublicKey, ToOtherAccount} 355