1import * as React from 'react' 2import * as Kb from '../../common-adapters' 3import * as Styles from '../../styles' 4import * as Container from '../../util/container' 5import * as SettingsGen from '../../actions/settings-gen' 6import {usePhoneNumberList} from '../../teams/common' 7import * as RPCGen from '../../constants/types/rpc-gen' 8import {pluralize} from '../../util/string' 9 10const shareURL = 'https://keybase.io/download?invite' 11const waitingKey = 'invitePeople' 12 13const InviteFriendsModal = () => { 14 const dispatch = Container.useDispatch() 15 const nav = Container.useSafeNavigation() 16 const onClose = () => dispatch(nav.safeNavigateUpPayload()) 17 const defaultCountry = Container.useSelector(s => s.settings.phoneNumbers.defaultCountry) 18 19 React.useEffect(() => { 20 // TODO: phone input should do this + remove from uses 21 if (!defaultCountry) { 22 dispatch(SettingsGen.createLoadDefaultPhoneNumberCountry()) 23 } 24 }, [defaultCountry, dispatch]) 25 26 const [emails, setEmails] = React.useState('') 27 const { 28 phoneNumbers, 29 setPhoneNumber, 30 addPhoneNumber, 31 removePhoneNumber, 32 resetPhoneNumbers, 33 } = usePhoneNumberList() 34 35 // disabled if both are empty or if there are some invalid phone numbers 36 const disabled = 37 (!emails && phoneNumbers.every(pn => !pn.phoneNumber)) || 38 phoneNumbers.some(pn => pn.phoneNumber && !pn.valid) 39 40 const submit = Container.useRPC(RPCGen.inviteFriendsInvitePeopleRpcPromise) 41 const [error, setError] = React.useState('') 42 const [successCount, setSuccessCount] = React.useState<number | null>(null) 43 const onSubmit = () => 44 submit( 45 [ 46 { 47 emails: {commaSeparatedEmailsFromUser: emails}, 48 phones: phoneNumbers.filter(p => !!p.phoneNumber).map(p => p.phoneNumber), 49 }, 50 waitingKey, 51 ], 52 r => { 53 setSuccessCount(r) 54 setError('') 55 setEmails('') 56 resetPhoneNumbers() 57 }, 58 err => { 59 setSuccessCount(null) 60 if (err.code === RPCGen.StatusCode.scratelimit) { 61 setError("You've been doing that a bit too much lately. Try again later.") 62 } else { 63 setError(err.message) 64 } 65 } 66 ) 67 const {popup, setShowingPopup} = Kb.usePopup(() => ( 68 <ShareLinkPopup onClose={() => setShowingPopup(false)} /> 69 )) 70 return ( 71 <Kb.Modal 72 mode="DefaultFullHeight" 73 onClose={onClose} 74 header={{ 75 leftButton: Styles.isMobile && ( 76 <Kb.Text type="BodyBigLink" onClick={onClose}> 77 Cancel 78 </Kb.Text> 79 ), 80 title: Styles.isMobile ? 'Invite friends' : 'Invite your friends to Keybase', 81 }} 82 footer={{ 83 content: ( 84 <Kb.Box2 direction="vertical" gap="medium" fullWidth={true}> 85 <Kb.WaitingButton 86 fullWidth={true} 87 type="Success" 88 label="Send invite" 89 disabled={disabled} 90 waitingKey={waitingKey} 91 onClick={onSubmit} 92 /> 93 {!Styles.isMobile && ( 94 <Kb.Box2 direction="vertical" gap="tiny" fullWidth={true}> 95 <Kb.Text type="BodySmall" center={true}> 96 or share a link: 97 </Kb.Text> 98 <Kb.CopyText text={shareURL} /> 99 </Kb.Box2> 100 )} 101 </Kb.Box2> 102 ), 103 }} 104 banners={[ 105 ...(error 106 ? [ 107 <Kb.Banner color="red" key="error" style={styles.banner}> 108 {error} 109 </Kb.Banner>, 110 ] 111 : []), 112 ...(successCount === null 113 ? [] 114 : [ 115 <Kb.Banner 116 color="green" 117 key="success" 118 style={styles.banner} 119 >{`Yeehaw! You invited ${successCount} ${pluralize('friend', successCount)}.`}</Kb.Banner>, 120 ]), 121 ]} 122 > 123 <Kb.Box2 direction="vertical" gap="small" fullWidth={true} alignItems="center" style={styles.container}> 124 <Kb.Box2 125 direction="vertical" 126 fullWidth={true} 127 centerChildren={true} 128 style={styles.illustrationContainer} 129 > 130 <Kb.Icon type="icon-illustration-invite-friends-460-96" /> 131 </Kb.Box2> 132 <Kb.Box2 direction="vertical" gap="small" fullWidth={true} style={styles.content}> 133 <Kb.Box2 direction="vertical" gap={Styles.isMobile ? 'xtiny' : 'tiny'} fullWidth={true}> 134 <Kb.Text type="BodySmallSemibold">By email address (separate with commas)</Kb.Text> 135 <Kb.LabeledInput 136 multiline={true} 137 hoverPlaceholder="Ex: cori@domain.com, paul@domain.com, etc." 138 placeholder="Email addresses" 139 rowsMin={3} 140 value={emails} 141 onChangeText={setEmails} 142 /> 143 </Kb.Box2> 144 <Kb.Box2 direction="vertical" gap={Styles.isMobile ? 'xtiny' : 'tiny'} fullWidth={true}> 145 <Kb.Text type="BodySmallSemibold">By phone number</Kb.Text> 146 <Kb.Box2 direction="vertical" gap="tiny" fullWidth={true}> 147 {phoneNumbers.map((pn, i) => ( 148 <Kb.PhoneInput 149 small={true} 150 key={pn.key} 151 defaultCountry={defaultCountry} 152 onChangeNumber={(phoneNumber, valid) => setPhoneNumber(i, phoneNumber, valid)} 153 onClear={phoneNumbers.length === 1 ? undefined : () => removePhoneNumber(i)} 154 /> 155 ))} 156 <Kb.Button 157 mode="Secondary" 158 icon="iconfont-new" 159 onClick={addPhoneNumber} 160 style={styles.alignSelfStart} 161 /> 162 </Kb.Box2> 163 </Kb.Box2> 164 {Styles.isMobile && ( 165 <Kb.ClickableBox style={styles.shareALink} onClick={() => setShowingPopup(true)}> 166 <Kb.Box2 direction="horizontal" gap="tiny" alignItems="center" alignSelf="flex-start"> 167 <Kb.Icon type="iconfont-link" color={Styles.globalColors.blueDark} /> 168 <Kb.Text type="BodyPrimaryLink">or share a link</Kb.Text> 169 </Kb.Box2> 170 {popup} 171 </Kb.ClickableBox> 172 )} 173 </Kb.Box2> 174 </Kb.Box2> 175 </Kb.Modal> 176 ) 177} 178 179export const ShareLinkPopup = ({onClose}: {onClose: () => void}) => ( 180 <Kb.MobilePopup> 181 <Kb.Box2 direction="vertical" style={styles.linkPopupContainer} gap="small" fullWidth={true}> 182 <Kb.Text type="Header">Share a link to Keybase</Kb.Text> 183 <Kb.CopyText text={shareURL} shareSheet={true} /> 184 <Kb.Button type="Dim" label="Close" fullWidth={true} onClick={onClose} /> 185 </Kb.Box2> 186 </Kb.MobilePopup> 187) 188 189const styles = Styles.styleSheetCreate(() => ({ 190 alignSelfStart: {alignSelf: 'flex-start'}, 191 banner: { 192 position: 'absolute', 193 top: 47, 194 zIndex: 1, 195 }, 196 container: { 197 backgroundColor: Styles.globalColors.blueGrey, 198 flex: 1, 199 }, 200 content: { 201 ...Styles.padding(0, Styles.globalMargins.small, Styles.globalMargins.small), 202 }, 203 illustrationContainer: { 204 backgroundColor: Styles.globalColors.purpleLight, 205 overflow: 'hidden', 206 }, 207 linkPopupContainer: { 208 ...Styles.padding(Styles.globalMargins.small), 209 }, 210 shareALink: { 211 ...Styles.padding(Styles.globalMargins.tiny, 0), 212 width: '100%', 213 }, 214})) 215 216export default InviteFriendsModal 217