[object Object]

← back to Costa Rica App

costa-rica-app: big BEST WAY TO CONTACT block on listing — WhatsApp/Call/Email/Message/Other color buttons + in-app messenger composer modal

e1db3036fbea4bca5e4d30bcb50d6a57da4e65d1 · 2026-08-07 11:44:24 -0700 · Steve Abrams

Files touched

Diff

commit e1db3036fbea4bca5e4d30bcb50d6a57da4e65d1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 11:44:24 2026 -0700

    costa-rica-app: big BEST WAY TO CONTACT block on listing — WhatsApp/Call/Email/Message/Other color buttons + in-app messenger composer modal
---
 app/listing/[slug].tsx | 81 ++++++++++++++++++++++++++++++++++++++++++++++++--
 src/api.ts             |  4 +++
 2 files changed, 82 insertions(+), 3 deletions(-)

diff --git a/app/listing/[slug].tsx b/app/listing/[slug].tsx
index ecd17b4..c793cf8 100644
--- a/app/listing/[slug].tsx
+++ b/app/listing/[slug].tsx
@@ -1,12 +1,25 @@
 import { useEffect, useState } from 'react';
-import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
+import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator, Linking, Alert, TextInput, Modal } from 'react-native';
 import { useLocalSearchParams, router } from 'expo-router';
-import { api, money } from '../../src/api';
+import { api, money, isAuthed } from '../../src/api';
 
 export default function ListingDetail() {
   const { slug } = useLocalSearchParams<{ slug: string }>();
   const [l, setL] = useState<any>(null);
-  useEffect(() => { api.listing(slug!).then(r => setL(r.listing)).catch(() => setL(false)); }, [slug]);
+  const [contact, setContact] = useState<any>(null);
+  const [msgOpen, setMsgOpen] = useState(false);
+  const [msg, setMsg] = useState('');
+  useEffect(() => {
+    api.listing(slug!).then(r => setL(r.listing)).catch(() => setL(false));
+    api.contact(slug!).then(r => setContact(r.contact)).catch(() => {});
+  }, [slug]);
+
+  async function sendMessage() {
+    if (!isAuthed()) { router.push('/login'); return; }
+    if (!msg.trim()) return;
+    try { await api.message(slug!, msg.trim()); setMsgOpen(false); setMsg(''); Alert.alert('Sent ✅', 'Your message was delivered. Replies show in your inbox.'); }
+    catch (e: any) { Alert.alert('Could not send', e.message); }
+  }
   if (l === null) return <ActivityIndicator style={{ marginTop: 40 }} />;
   if (l === false) return <Text style={s.err}>Not available.</Text>;
   return (
@@ -22,7 +35,54 @@ export default function ListingDetail() {
           <Text style={s.small}>Up to {l.max_guests} guests · {l.instant_book ? 'Instant book' : 'Request to book'} · {l.cancellation}</Text>
         </View>
         {l.address ? <Text style={s.small}>📍 {l.address}</Text> : null}
+
+        {/* BEST WAY TO CONTACT — big, prominent */}
+        <Text style={s.contactHead}>BEST WAY TO CONTACT</Text>
+        <View style={s.contactGrid}>
+          {contact?.whatsapp && (
+            <TouchableOpacity style={[s.cbtn, s.wa]} onPress={() => Linking.openURL(contact.whatsapp)}>
+              <Text style={s.cemoji}>💬</Text><Text style={s.cbtnTxt}>WhatsApp</Text>
+            </TouchableOpacity>
+          )}
+          {contact?.tel && (
+            <TouchableOpacity style={[s.cbtn, s.call]} onPress={() => Linking.openURL(contact.tel)}>
+              <Text style={s.cemoji}>📞</Text><Text style={s.cbtnTxt}>Call</Text>
+            </TouchableOpacity>
+          )}
+          {contact?.mailto && (
+            <TouchableOpacity style={[s.cbtn, s.email]} onPress={() => Linking.openURL(contact.mailto)}>
+              <Text style={s.cemoji}>✉️</Text><Text style={s.cbtnTxt}>Email</Text>
+            </TouchableOpacity>
+          )}
+          <TouchableOpacity style={[s.cbtn, s.msg]} onPress={() => (isAuthed() ? setMsgOpen(true) : router.push('/login'))}>
+            <Text style={s.cemoji}>📨</Text><Text style={s.cbtnTxt}>Message</Text>
+          </TouchableOpacity>
+          {contact?.website && (
+            <TouchableOpacity style={[s.cbtn, s.other]} onPress={() => Linking.openURL(contact.website)}>
+              <Text style={s.cemoji}>🌐</Text><Text style={s.cbtnTxt}>Other</Text>
+            </TouchableOpacity>
+          )}
+        </View>
       </ScrollView>
+
+      {/* In-app messenger composer */}
+      <Modal visible={msgOpen} animationType="slide" transparent onRequestClose={() => setMsgOpen(false)}>
+        <View style={s.modalWrap}>
+          <View style={s.modalCard}>
+            <Text style={s.modalH}>Message {l.name}</Text>
+            <TextInput style={s.msgInput} placeholder="Ask about availability, price, anything…" value={msg}
+              onChangeText={setMsg} multiline autoFocus />
+            <View style={{ flexDirection: 'row', gap: 8, marginTop: 12 }}>
+              <TouchableOpacity style={[s.cta, { flex: 1, backgroundColor: '#eee', marginTop: 0 }]} onPress={() => setMsgOpen(false)}>
+                <Text style={[s.ctaTxt, { color: '#333' }]}>Cancel</Text>
+              </TouchableOpacity>
+              <TouchableOpacity style={[s.cta, { flex: 1, marginTop: 0 }]} onPress={sendMessage}>
+                <Text style={s.ctaTxt}>Send</Text>
+              </TouchableOpacity>
+            </View>
+          </View>
+        </View>
+      </Modal>
       <TouchableOpacity style={s.cta} onPress={() => router.push(`/book/${slug}`)}>
         <Text style={s.ctaTxt}>Book now</Text>
       </TouchableOpacity>
@@ -42,4 +102,19 @@ const s = StyleSheet.create({
   cta: { backgroundColor: '#0a7d55', margin: 16, height: 52, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
   ctaTxt: { color: '#fff', fontSize: 17, fontWeight: '700' },
   err: { textAlign: 'center', marginTop: 40, color: '#a33' },
+  // BEST WAY TO CONTACT
+  contactHead: { fontSize: 22, fontWeight: '900', letterSpacing: 1, color: '#1a2b22', marginTop: 26, marginBottom: 12, textAlign: 'center' },
+  contactGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, justifyContent: 'center' },
+  cbtn: { width: '47%', minHeight: 74, borderRadius: 14, alignItems: 'center', justifyContent: 'center', paddingVertical: 12 },
+  cemoji: { fontSize: 26 },
+  cbtnTxt: { color: '#fff', fontWeight: '800', fontSize: 16, marginTop: 4 },
+  wa: { backgroundColor: '#25D366' },
+  call: { backgroundColor: '#0a7d55' },
+  email: { backgroundColor: '#2b6cb0' },
+  msg: { backgroundColor: '#7a3cff' },
+  other: { backgroundColor: '#555' },
+  modalWrap: { flex: 1, backgroundColor: 'rgba(0,0,0,.4)', justifyContent: 'flex-end' },
+  modalCard: { backgroundColor: '#fff', borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 20 },
+  modalH: { fontSize: 18, fontWeight: '800', color: '#1a2b22', marginBottom: 12 },
+  msgInput: { minHeight: 90, borderWidth: 1, borderColor: '#e2e5e0', borderRadius: 12, padding: 12, textAlignVertical: 'top' },
 });
diff --git a/src/api.ts b/src/api.ts
index 93225f6..1096df9 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -32,6 +32,10 @@ export const api = {
   me: () => req('/me'),
   listings: (q: Record<string, string> = {}) => req('/listings?' + new URLSearchParams(q).toString()),
   listing: (slug: string) => req(`/listings/${slug}`),
+  contact: (slug: string) => req(`/listings/${slug}/contact`),
+  message: (slug: string, body: string) => req(`/listings/${slug}/message`, { method: 'POST', body: JSON.stringify({ body }) }),
+  threads: () => req('/threads'),
+  thread: (id: number) => req(`/threads/${id}`),
   availability: (slug: string, from?: string, to?: string) =>
     req(`/listings/${slug}/availability?` + new URLSearchParams({ ...(from && { from }), ...(to && { to }) } as any)),
   createBooking: (b: any) => req('/bookings', { method: 'POST', body: JSON.stringify(b) }),

← 30a7d50 costa-rica-app: align react-native 0.76.9 + react-native-scr  ·  back to Costa Rica App  ·  costa-rica-app: add missing expo-asset + expo-font (Metro/EA a7eb8a4 →