/* global alert */ import React, { Component } from 'react'; import { Alert, FlatList, Keyboard, KeyboardAvoidingView, Linking, Platform, StyleSheet, Text, TouchableHighlight, TouchableOpacity, View, } from 'react-native'; import { BlueButton, BlueCopyTextToClipboard, BlueLoading, BlueNavigationStyle, BlueSpacing10, BlueSpacing20, BlueText, } from '../../BlueComponents'; import { HodlHodlApi } from '../../class/hodl-hodl-api'; import * as NavigationService from '../../NavigationService'; import { BlueCurrentTheme } from '../../components/themes'; import BottomModal from '../../components/BottomModal'; import loc from '../../loc'; import { BlueStorageContext } from '../../blue_modules/storage-context'; export default class HodlHodlMyContracts extends Component { static contextType = BlueStorageContext; constructor(props) { super(props); this.state = { contracts: [], isLoading: true, }; } componentWillUnmount() { clearInterval(this.state.inverval); } async componentDidMount() { const hodlApiKey = await this.context.getHodlHodlApiKey(); const hodlApi = new HodlHodlApi(hodlApiKey); this.setState({ hodlApi: hodlApi, contracts: [] }); const inverval = setInterval(async () => { await this.refetchContracts(); }, 60 * 1000); this.setState({ inverval }); await this.refetchContracts(); } render() { if (this.state.isLoading) return ; return ( { return item.id; }} ListEmptyComponent={() => {loc.hodl.cont_no}} style={styles.flatList} ItemSeparatorComponent={() => } data={this.state.contracts} renderItem={({ item: contract, index, separators }) => ( this._onContractPress(contract)} > {contract.status} {contract.volume_breakdown.goes_to_buyer} {contract.asset_code} {contract.your_role === 'buyer' ? loc.hodl.cont_buying : loc.hodl.cont_selling} {contract.statusText} )} /> {this.renderContract()} ); } async refetchContracts() { this.setState({ isLoading: true, }); const hodlApi = this.state.hodlApi; let contracts = []; let contractToDisplay = this.state.contractToDisplay; const contractIds = await this.context.getHodlHodlContracts(); /* * Initiator sends “Getting contract” request once every 1-3 minutes until contract.escrow.address is not null (thus, waiting for offer’s creator to confirm his payment password in case he uses the website) * Each party verifies the escrow address locally * Each party sends “Confirming contract’s escrow validity” request to the server */ for (const id of contractIds) { let contract; try { contract = await hodlApi.getContract(id); } catch (_) { continue; } if (contract.status === 'canceled') continue; if (contract.escrow && contract.escrow.address && hodlApi.verifyEscrowAddress()) { await hodlApi.markContractAsConfirmed(id); contract.isDepositedEnought = contract.escrow.confirmations >= contract.confirmations && +contract.escrow.amount_deposited >= +contract.volume; // technically, we could fetch balance of escrow address ourselved and verify, but we are relying on api here contract.statusText = loc.hodl.cont_st_waiting; if (contract.isDepositedEnought && contract.status !== 'paid') contract.statusText = loc.hodl.cont_st_paid_enought; if (contract.status === 'paid') contract.statusText = loc.hodl.cont_st_paid_waiting; if (contract.status === 'in_progress' && contract.your_role === 'buyer') contract.statusText = loc.hodl.cont_st_in_progress_buyer; if (contract.status === 'completed') contract.statusText = loc.hodl.cont_st_completed; } contracts.push(contract); if (contractToDisplay && contract.id === this.state.contractToDisplay.id) { // refreshing contract that is currently being displayed contractToDisplay = contract; } } contracts = contracts.sort((a, b) => (a.created_at >= b.created_at ? -1 : 1)); // new contracts on top this.setState({ hodlApi: hodlApi, contracts, contractToDisplay, isLoading: false }); } _onContractPress(contract) { this.setState({ contractToDisplay: contract, isRenderContractVisible: true, }); } hideContractModal = () => { Keyboard.dismiss(); this.setState({ isRenderContractVisible: false }); }; renderContract = () => { if (!this.state.contractToDisplay) return; return ( {this.state.contractToDisplay.volume_breakdown.goes_to_buyer} {this.state.contractToDisplay.asset_code} {this.state.contractToDisplay.price} {this.state.contractToDisplay.currency_code} {loc.hodl.cont_address_to} Linking.openURL(`https://blockstream.info/address/${this.state.contractToDisplay.release_address}`)} > {this.state.contractToDisplay.release_address} {loc.hodl.cont_address_escrow} Linking.openURL(`https://blockstream.info/address/${this.state.contractToDisplay.escrow.address}`)} > {this.state.contractToDisplay.escrow.address} {this.isAllowedToMarkContractAsPaid() ? ( {loc.hodl.cont_how} ) : ( )} {this.isAllowedToMarkContractAsPaid() ? ( this._onMarkContractAsPaid()} /> ) : ( )} {this.state.contractToDisplay.can_be_canceled && ( this._onCancelContract()} style={styles.cancelContractText}> {loc.hodl.cont_cancel} )} this._onOpenContractOnWebsite()} style={styles.openChatText}> {loc.hodl.cont_chat} ); }; /** * If you are the buyer, DO NOT SEND PAYMENT UNTIL CONTRACT STATUS IS "in_progress". */ _onMarkContractAsPaid() { if (!this.state.contractToDisplay) return; Alert.alert( loc.hodl.cont_paid_q, loc.hodl.cont_paid_e, [ { text: loc._.yes, onPress: async () => { const hodlApi = this.state.hodlApi; try { await hodlApi.markContractAsPaid(this.state.contractToDisplay.id); this.setState({ isRenderContractVisible: false }); await this.refetchContracts(); } catch (Error) { alert(Error); } }, style: 'default', }, { text: loc._.cancel, onPress: () => {}, style: 'cancel', }, ], { cancelable: true }, ); } async _onOpenContractOnWebsite() { if (!this.state.contractToDisplay) return; const hodlApi = this.state.hodlApi; const sigKey = await this.context.getHodlHodlSignatureKey(); if (!sigKey) { alert('Error: signature key not set'); // should never happen return; } const autologinKey = await hodlApi.requestAutologinToken(sigKey); const uri = 'https://hodlhodl.com/contracts/' + this.state.contractToDisplay.id + '?sign_in_token=' + autologinKey; this.setState({ isRenderContractVisible: false }, () => { NavigationService.navigate('HodlHodlWebview', { uri }); }); } _onCancelContract() { if (!this.state.contractToDisplay) return; Alert.alert( loc.hodl.cont_cancel_q, '', [ { text: loc.hodl.cont_cancel_y, onPress: async () => { const hodlApi = this.state.hodlApi; try { await hodlApi.cancelContract(this.state.contractToDisplay.id); this.setState({ isRenderContractVisible: false }); await this.refetchContracts(); } catch (Error) { alert(Error); } }, style: 'default', }, { text: loc._.cancel, onPress: () => {}, style: 'cancel', }, ], { cancelable: true }, ); } isAllowedToMarkContractAsPaid() { return this.state.contractToDisplay.status === 'in_progress' && this.state.contractToDisplay.your_role === 'buyer'; } } const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: BlueCurrentTheme.colors.elevated, }, modalContent: { backgroundColor: BlueCurrentTheme.colors.modal, padding: 22, borderTopLeftRadius: 16, borderTopRightRadius: 16, borderColor: 'rgba(0, 0, 0, 0.1)', minHeight: 425, }, modalContentCentered: { justifyContent: 'center', alignItems: 'center', }, statusGreenWrapper: { backgroundColor: BlueCurrentTheme.colors.feeLabel, borderRadius: 20, height: 28, justifyContent: 'center', alignItems: 'center', margin: 15, paddingLeft: 15, paddingRight: 15, }, statusGreenText: { fontSize: 12, color: BlueCurrentTheme.colors.feeValue, }, statusGrayWrapper: { backgroundColor: BlueCurrentTheme.colors.lightBorder, borderRadius: 20, height: 28, justifyContent: 'center', alignItems: 'center', margin: 15, paddingLeft: 15, paddingRight: 15, }, statusGrayText: { fontSize: 12, color: '#9AA0AA', }, statusGrayWrapper2: { backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor, borderRadius: 5, minHeight: 28, maxHeight: 56, justifyContent: 'center', alignItems: 'center', paddingLeft: 15, paddingRight: 15, }, statusGrayText2: { fontSize: 12, color: '#9AA0AA', }, btcText: { fontWeight: 'bold', fontSize: 18, color: BlueCurrentTheme.colors.foregroundColor, }, subheaderText: { fontSize: 12, fontWeight: 'bold', color: BlueCurrentTheme.colors.feeText, }, loading: { backgroundColor: BlueCurrentTheme.colors.elevated }, emptyComponentText: { textAlign: 'center', color: '#9AA0AA', paddingHorizontal: 16, backgroundColor: BlueCurrentTheme.colors.elevated }, itemSeparatorComponent: { height: 0.5, width: '100%', backgroundColor: '#C8C8C8' }, flexDirectionRow: { flexDirection: 'row' }, flexDirectionColumn: { flexDirection: 'column' }, volumeBreakdownText: { fontSize: 18, color: BlueCurrentTheme.colors.foregroundColor }, contractStatusText: { fontSize: 13, color: 'gray', fontWeight: 'normal' }, cancelContractText: { color: '#d0021b', fontSize: 15, paddingTop: 20, fontWeight: '500', textAlign: 'center' }, openChatText: { color: BlueCurrentTheme.colors.foregroundColor, fontSize: 15, paddingTop: 20, fontWeight: '500', textAlign: 'center' }, flatList: { paddingTop: 30, backgroundColor: BlueCurrentTheme.colors.elevated }, roleText: { fontSize: 14, color: 'gray', padding: 5 }, marginRight: { marginRight: 20, }, }); HodlHodlMyContracts.navigationOptions = ({ navigation }) => ({ ...BlueNavigationStyle(navigation, true), title: loc.hodl.cont_title, headerStyle: { backgroundColor: BlueCurrentTheme.colors.elevated, }, headerRight: () => ( { Alert.alert( loc.hodl.are_you_sure_you_want_to_logout, '', [ { text: loc._.ok, onPress: () => { this.context.setHodlHodlApiKey('', ''); navigation.navigate('WalletsList'); }, style: 'default', }, { text: loc._.cancel, onPress: () => {}, style: 'cancel' }, ], { cancelable: false }, ); }} > logout ), });