waitlist.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <?php
  2. /**
  3. * waitlist.php — Waitlist signup endpoint
  4. * ----------------------------------------
  5. * Accepts POST with JSON { "email": "...", "source": "..." }
  6. * Validates the email, stores to telemetry DB (existing), and
  7. * sends a notification email via PHPMailer + SMTP.
  8. *
  9. * Environment variables (set in docker-compose.yml):
  10. * SMTP_HOST — e.g. smtp.gmail.com or smtp.sendgrid.net
  11. * SMTP_PORT — 587 (TLS) or 465 (SSL), default 587
  12. * SMTP_USER — SMTP username / email address
  13. * SMTP_PASS — SMTP password or app password
  14. * SMTP_FROM — From address, default: SMTP_USER
  15. * SMTP_FROM_NAME — From name, default: "Tas Planning Assistant"
  16. * NOTIFY_EMAIL — Where to send admin notifications (your inbox)
  17. * APP_API_BASE — API base for telemetry fallback
  18. */
  19. declare(strict_types=1);
  20. header('Content-Type: application/json; charset=utf-8');
  21. header('Access-Control-Allow-Methods: POST, OPTIONS');
  22. header('Access-Control-Allow-Headers: Content-Type');
  23. // Handle preflight
  24. if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
  25. http_response_code(204);
  26. exit;
  27. }
  28. if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
  29. http_response_code(405);
  30. echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
  31. exit;
  32. }
  33. require_once __DIR__ . '/vendor/autoload.php';
  34. use PHPMailer\PHPMailer\PHPMailer;
  35. use PHPMailer\PHPMailer\SMTP;
  36. use PHPMailer\PHPMailer\Exception as MailException;
  37. // ── Input ──────────────────────────────────────────────────────────────
  38. $raw = file_get_contents('php://input') ?: '{}';
  39. $input = json_decode($raw, true) ?: [];
  40. $email = trim((string)($input['email'] ?? $_POST['email'] ?? ''));
  41. $source = trim((string)($input['source'] ?? $_POST['source'] ?? 'waitlist'));
  42. $name = trim((string)($input['name'] ?? $_POST['name'] ?? ''));
  43. // ── Validate ───────────────────────────────────────────────────────────
  44. if (!$email) {
  45. http_response_code(400);
  46. echo json_encode(['ok' => false, 'error' => 'Email address is required']);
  47. exit;
  48. }
  49. if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
  50. http_response_code(422);
  51. echo json_encode(['ok' => false, 'error' => 'Please enter a valid email address']);
  52. exit;
  53. }
  54. // Basic honeypot / rate limit: reject obviously bad inputs
  55. if (strlen($email) > 254 || preg_match('/[<>"\']/', $email)) {
  56. http_response_code(422);
  57. echo json_encode(['ok' => false, 'error' => 'Invalid email address']);
  58. exit;
  59. }
  60. // ── SMTP config from environment ───────────────────────────────────────
  61. $smtpHost = $_SERVER['SMTP_HOST'] ?? $_ENV['SMTP_HOST'] ?? getenv('SMTP_HOST') ?: '';
  62. $smtpPort = (int)($_SERVER['SMTP_PORT'] ?? $_ENV['SMTP_PORT'] ?? getenv('SMTP_PORT') ?: 587);
  63. $smtpUser = $_SERVER['SMTP_USER'] ?? $_ENV['SMTP_USER'] ?? getenv('SMTP_USER') ?: '';
  64. $smtpPass = $_SERVER['SMTP_PASS'] ?? $_ENV['SMTP_PASS'] ?? getenv('SMTP_PASS') ?: '';
  65. $smtpFrom = $_SERVER['SMTP_FROM'] ?? $_ENV['SMTP_FROM'] ?? getenv('SMTP_FROM') ?: $smtpUser;
  66. $smtpFromName = $_SERVER['SMTP_FROM_NAME'] ?? $_ENV['SMTP_FROM_NAME'] ?? getenv('SMTP_FROM_NAME') ?: 'Tas Planning Assistant';
  67. $notifyEmail = $_SERVER['NOTIFY_EMAIL'] ?? $_ENV['NOTIFY_EMAIL'] ?? getenv('NOTIFY_EMAIL') ?: $smtpUser;
  68. $errors = [];
  69. // ── 1. Send confirmation email to subscriber ───────────────────────────
  70. if ($smtpHost && $smtpUser) {
  71. try {
  72. $mail = new PHPMailer(true);
  73. $mail->isSMTP();
  74. $mail->Host = $smtpHost;
  75. $mail->SMTPAuth = true;
  76. $mail->Username = $smtpUser;
  77. $mail->Password = $smtpPass;
  78. $mail->SMTPSecure = $smtpPort === 465 ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
  79. $mail->Port = $smtpPort;
  80. $mail->CharSet = 'UTF-8';
  81. $mail->setFrom($smtpFrom, $smtpFromName);
  82. $mail->addAddress($email, $name ?: '');
  83. $mail->addReplyTo($smtpFrom, $smtpFromName);
  84. $mail->isHTML(true);
  85. $mail->Subject = "You're on the waitlist — Tasmania's AI Planning Scheme Pro";
  86. $mail->Body = confirmationHtml($email, $name);
  87. $mail->AltBody = confirmationText($email, $name);
  88. $mail->send();
  89. } catch (MailException $e) {
  90. // Log but don't fail the whole request — notification is non-critical
  91. $errors[] = 'confirmation_mail: ' . $mail->ErrorInfo;
  92. error_log('[waitlist] Confirmation mail failed: ' . $mail->ErrorInfo);
  93. }
  94. // ── 2. Send admin notification ─────────────────────────────────────
  95. if ($notifyEmail) {
  96. try {
  97. $notify = new PHPMailer(true);
  98. $notify->isSMTP();
  99. $notify->Host = $smtpHost;
  100. $notify->SMTPAuth = true;
  101. $notify->Username = $smtpUser;
  102. $notify->Password = $smtpPass;
  103. $notify->SMTPSecure = $smtpPort === 465 ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
  104. $notify->Port = $smtpPort;
  105. $notify->CharSet = 'UTF-8';
  106. $notify->setFrom($smtpFrom, $smtpFromName);
  107. $notify->addAddress($notifyEmail);
  108. $notify->Subject = "New waitlist signup: {$email}";
  109. $notify->Body = "<p><strong>Email:</strong> {$email}</p>"
  110. . "<p><strong>Source:</strong> {$source}</p>"
  111. . "<p><strong>Time:</strong> " . date('d M Y H:i:s T') . "</p>";
  112. $notify->AltBody = "New waitlist signup\nEmail: {$email}\nSource: {$source}\nTime: " . date('d M Y H:i:s T');
  113. $notify->isHTML(true);
  114. $notify->send();
  115. } catch (MailException $e) {
  116. $errors[] = 'notify_mail: ' . $notify->ErrorInfo;
  117. error_log('[waitlist] Notify mail failed: ' . $notify->ErrorInfo);
  118. }
  119. }
  120. } else {
  121. // No SMTP configured — log a warning but still return success
  122. error_log('[waitlist] SMTP not configured. Set SMTP_HOST and SMTP_USER in environment.');
  123. $errors[] = 'smtp_not_configured';
  124. }
  125. // ── 3. Log to telemetry DB via the API (best-effort) ───────────────────
  126. $apiBase = $_SERVER['APP_API_BASE'] ?? $_ENV['APP_API_BASE'] ?? getenv('APP_API_BASE') ?: 'https://api.modulos.com.au/ask';
  127. $telUrl = preg_replace('/\/ask\/?$/', '', $apiBase) . '/telemetry';
  128. $telPayload = json_encode([
  129. 'type' => 'waitlist_join',
  130. 'email' => $email,
  131. 'source'=> $source,
  132. 'ts' => date('c'),
  133. ]);
  134. // Fire-and-forget with a short timeout
  135. $ctx = stream_context_create(['http' => [
  136. 'method' => 'POST',
  137. 'header' => "Content-Type: application/json\r\n",
  138. 'content' => $telPayload,
  139. 'timeout' => 2,
  140. 'ignore_errors' => true,
  141. ]]);
  142. @file_get_contents($telUrl, false, $ctx);
  143. // ── Response ───────────────────────────────────────────────────────────
  144. echo json_encode([
  145. 'ok' => true,
  146. 'message' => "You're on the list! We'll be in touch when Pro launches.",
  147. 'warnings'=> $errors ?: null,
  148. ]);
  149. // ── Email templates ────────────────────────────────────────────────────
  150. function confirmationHtml(string $email, string $name = ''): string {
  151. $greeting = $name ? "Hi {$name}," : 'Hi there,';
  152. return <<<HTML
  153. <!DOCTYPE html>
  154. <html>
  155. <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
  156. <body style="margin:0;padding:0;background:#f4f4f4;font-family:'Segoe UI',Arial,sans-serif;">
  157. <table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f4;padding:32px 16px;">
  158. <tr><td align="center">
  159. <table width="580" cellpadding="0" cellspacing="0" style="background:#0b0f0e;border-radius:12px;overflow:hidden;max-width:580px;">
  160. <!-- Header -->
  161. <tr>
  162. <td style="background:#141a17;padding:28px 32px;border-bottom:1px solid rgba(255,255,255,0.07);">
  163. <table cellpadding="0" cellspacing="0">
  164. <tr>
  165. <td style="background:rgba(45,220,138,0.1);border:1px solid rgba(45,220,138,0.25);border-radius:6px;width:28px;height:28px;text-align:center;vertical-align:middle;">
  166. <span style="color:#2ddc8a;font-size:14px;font-weight:bold;">▲</span>
  167. </td>
  168. <td style="padding-left:10px;color:#eaf0ec;font-size:14px;font-weight:500;">
  169. Tasmanian Planning Scheme
  170. </td>
  171. </tr>
  172. </table>
  173. </td>
  174. </tr>
  175. <!-- Body -->
  176. <tr>
  177. <td style="padding:36px 32px;">
  178. <h1 style="color:#eaf0ec;font-size:22px;font-weight:400;margin:0 0 16px;line-height:1.3;">
  179. You're on the <span style="color:#2ddc8a;font-style:italic;">Pro waitlist</span>
  180. </h1>
  181. <p style="color:#8fa899;font-size:15px;line-height:1.7;margin:0 0 16px;">
  182. {$greeting}
  183. </p>
  184. <p style="color:#8fa899;font-size:15px;line-height:1.7;margin:0 0 24px;">
  185. Thanks for joining the waitlist for <strong style="color:#eaf0ec;">Tasmanian Planning Scheme Pro</strong>.
  186. We'll email you as soon as it's available — including early-access pricing for waitlist members.
  187. </p>
  188. <!-- Feature list -->
  189. <table cellpadding="0" cellspacing="0" style="background:#141a17;border:1px solid rgba(255,255,255,0.07);border-radius:10px;width:100%;margin-bottom:28px;">
  190. <tr><td style="padding:20px 24px;">
  191. <p style="color:#4f6459;font-size:11px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;margin:0 0 14px;">What's included in Pro</p>
  192. <table cellpadding="0" cellspacing="0" width="100%">
  193. <tr><td style="padding:5px 0;color:#8fa899;font-size:13px;">✓ &nbsp;Full planning report from one brief</td></tr>
  194. <tr><td style="padding:5px 0;color:#8fa899;font-size:13px;">✓ &nbsp;Zone + codes tables with A/P assessment</td></tr>
  195. <tr><td style="padding:5px 0;color:#8fa899;font-size:13px;">✓ &nbsp;Clause-linked sources throughout</td></tr>
  196. <tr><td style="padding:5px 0;color:#8fa899;font-size:13px;">✓ &nbsp;Google Docs export</td></tr>
  197. <tr><td style="padding:5px 0;color:#8fa899;font-size:13px;">✓ &nbsp;NCC/AS hooks when released</td></tr>
  198. </table>
  199. </td></tr>
  200. </table>
  201. <p style="color:#8fa899;font-size:14px;line-height:1.7;margin:0 0 28px;">
  202. In the meantime, the free assistant is available at
  203. <a href="https://tasplanning.report" style="color:#2ddc8a;text-decoration:none;">tasplanning.report</a>
  204. — ask questions about zones, overlays, setbacks, and acceptable solutions with full clause citations.
  205. </p>
  206. <a href="https://tasplanning.report/local_state-planning-scheme.php"
  207. style="display:inline-block;background:#2ddc8a;color:#0b0f0e;font-size:14px;font-weight:500;
  208. padding:12px 24px;border-radius:8px;text-decoration:none;">
  209. Try the free assistant →
  210. </a>
  211. </td>
  212. </tr>
  213. <!-- Footer -->
  214. <tr>
  215. <td style="padding:20px 32px;border-top:1px solid rgba(255,255,255,0.07);">
  216. <p style="color:#4f6459;font-size:12px;margin:0;line-height:1.6;">
  217. You're receiving this because you signed up at tasplanning.report.<br>
  218. To unsubscribe, reply to this email with "unsubscribe" in the subject.
  219. </p>
  220. </td>
  221. </tr>
  222. </table>
  223. </td></tr>
  224. </table>
  225. </body>
  226. </html>
  227. HTML;
  228. }
  229. function confirmationText(string $email, string $name = ''): string {
  230. $greeting = $name ? "Hi {$name}," : 'Hi there,';
  231. return <<<TEXT
  232. {$greeting}
  233. You're on the waitlist for Tasmanian Planning Scheme Pro.
  234. We'll email you as soon as it's available — including early-access pricing for waitlist members.
  235. What's included in Pro:
  236. - Full planning report from one brief
  237. - Zone + codes tables with A/P assessment
  238. - Clause-linked sources throughout
  239. - Google Docs export
  240. - NCC/AS hooks when released
  241. In the meantime, the free assistant is available at https://tasplanning.report
  242. ---
  243. You're receiving this because you signed up at tasplanning.report.
  244. To unsubscribe, reply with "unsubscribe" in the subject.
  245. TEXT;
  246. }