← back to Kickbacks Ai Upstream

src/consent/prompt.ts

74 lines

/** First-run consent prompt (wave-2I-F03).
 *
 * Surfaces a one-time vscode.window.showInformationMessage AFTER sign-in,
 * once per (extension instance × tos_version) tuple. Stores "shown for
 * version X" in globalState so we don't nag the same user on every reload.
 *
 * If the user clicks Agree, we POST /v1/me/consent (server stamps the
 * version). If they click Privacy, we open the privacy doc and leave the
 * prompt unflagged so it surfaces again next session (gentle, not pushy).
 * If they dismiss, we set "shown" so we don't nag this session.
 */
import type * as vscode from "vscode";
import type { ConsentClient, ConsentState } from "./client";

const SHOWN_KEY = "vibe-ads.consent.promptShownForVersion";
const PRIVACY_URL = "https://kickbacks.ai/privacy"; // hosted PRIVACY.md target

export interface ConsentPromptOptions {
  client: ConsentClient;
  ctx: vscode.ExtensionContext;
  vsc: typeof vscode;
  // dlog factory injected so unit tests can spy
  dlog?: (msg: string) => void;
}

export async function maybePromptForConsent(opts: ConsentPromptOptions): Promise<void> {
  const { client, ctx, vsc, dlog } = opts;
  // The caller fires this without awaiting (`void maybePromptForConsent(...)`)
  // — a rejection anywhere below would surface as an unhandledRejection, so
  // the whole body is guarded. Consent prompting is best-effort by contract.
  try {
    const state = await client.read();
    if (state === null) return; // signed out or backend unreachable; try later
    // Already consented to the live version → nothing to do.
    if (state.telemetryOptIn
        && state.tosAcceptedVersion === state.currentTosVersion) {
      return;
    }
    const shownFor = ctx.globalState.get<string>(SHOWN_KEY);
    if (shownFor === state.currentTosVersion) {
      // Already nagged for this version this session/process; defer to next.
      return;
    }
    dlog?.(`consent.prompt show version=${state.currentTosVersion}`);
    const pick = await vsc.window.showInformationMessage(
      "Kickbacks shows subtle ads in the Claude Code spinner and splits "
      + "50/50 of every settled dollar back to you. Telemetry is opt-in. "
      + "Continue?",
      { modal: false },
      "Agree",
      "Privacy Policy",
    );
    if (pick === "Agree") {
      const result = await client.accept();
      dlog?.(`consent.prompt accept result=${result ? "ok" : "fail"}`);
      if (result) {
        // Mark shown only after successful POST to avoid stranding the user
        // in a partial-accept state.
        void ctx.globalState.update(SHOWN_KEY, result.tosVersion);
      }
      return;
    }
    if (pick === "Privacy Policy") {
      void vsc.env.openExternal(vsc.Uri.parse(PRIVACY_URL));
      // Don't mark shown — surface again next session.
      return;
    }
    // Dismissed -> set "shown" for this version so we don't pester.
    void ctx.globalState.update(SHOWN_KEY, state.currentTosVersion);
  } catch (e) {
    dlog?.(`consent.prompt error: ${e instanceof Error ? e.message : String(e)}`);
  }
}