Menu

1.2.0

注目すべき点

バージョン番号については、package.jsonファイルのversionフィールドをご確認ください

価格管理でクーポン設定をサポート

prices-coupon

実装された機能:

  • Stripeで作成されたクーポンを取得
  • 選択可能なクーポン
  • Stripeで作成された商品価格を元の価格として表示し、現在の価格はクーポン適用後の計算値
  • クーポンが設定されている場合、ユーザーが購入ボタンをクリックすると、手動入力なしでクーポンが自動適用される
  • 手動クーポン入力購入エントリの表示オプション

更新手順

ステップ1:Supabase SQL Editorで以下のコマンドを実行してテーブルフィールドを追加

ALTER TABLE public.pricing_plans
ADD COLUMN stripe_coupon_id character varying(255) NULL,
ADD COLUMN enable_manual_input_coupon boolean DEFAULT false NOT NULL;
 
COMMENT ON COLUMN public.pricing_plans.stripe_coupon_id IS 'The ID of the Stripe coupon associated with this plan.'; 

ステップ2:コマンドを実行してSupabaseタイプを更新

supabase gen types typescript --project-id <your-project-id> --schema public > lib/supabase/types.ts

ステップ3:types/pricing.ts定義を修正

types/pricing.ts
export interface PricingPlan {
  // ...その他のコード...
  stripe_coupon_id?: string | null; // 追加
  enable_manual_input_coupon?: boolean; // 追加
}

ステップ4:actions/prices/admin.tsの価格プラン作成メソッドを修正

actions/prices/admin.ts
export async function createPricingPlanAction({
  planData,
  locale = DEFAULT_LOCALE,
}: CreatePricingPlanParams) {
  // ...その他のコード...
 
    const { data, error } = await supabaseAdmin
      .from("pricing_plans")
      .insert({
        // ...その他のコード...
        stripe_coupon_id: planData.stripe_coupon_id,
        enable_manual_input_coupon: planData.enable_manual_input_coupon ?? false,
      })
 
  // ...その他のコード...
}

ステップ5:/prices/PricePlanForm.tsxを修正して新しい要件を拡張

app/[locale]/(protected)/dashboard/(admin)/prices/PricePlanForm.tsx
// 1. フォームタイプ定義を拡張
const pricingPlanFormSchema = z.object({
  // ...その他のコード...
  stripe_coupon_id: z.string().optional().nullable(),
  enable_manual_input_coupon: z.boolean().optional().nullable(),
})
 
// 2. クーポン機能関連の状態を提供
  const [isFetchingCoupons, setIsFetchingCoupons] = useState(false);
  const [coupons, setCoupons] = useState<any[]>([]);
 
// 3. フォーム定義を拡張
  const form = useForm<PricingPlanFormValues>({
    resolver: zodResolver(pricingPlanFormSchema),
    defaultValues: {
      // ... その他のコード ...
      stripe_coupon_id: initialData?.stripe_coupon_id ?? "",
      enable_manual_input_coupon: initialData?.enable_manual_input_coupon ?? false,
    }
  })
 
// 4. stripe_coupon_idの変更を監視して表示価格を計算
  const watchStripeCouponId = form.watch("stripe_coupon_id");
 
// 5. クーポン関連メソッドを追加
  useEffect(() => {
    // ソースコードを直接確認し、完全なメソッドをコピーしてください
  }, [watchEnvironment]);
 
  useEffect(() => {
    // ソースコードを直接確認し、完全なメソッドをコピーしてください
  }, [watchStripeCouponId, coupons]);
 
  const handleFetchCoupons = async () => {
    // ソースコードを直接確認し、完全なメソッドをコピーしてください
  };
 
// 6. 表示価格と元の価格のカスタム表示ロジックを最適化
 
  const handleStripeVerify = async () => {
  // ... その他のコード ...
 
    // 削除
    // if (!form.getValues("display_price")) {
    //   const formattedPrice = await formatCurrency(
    //     priceInCorrectUnit,
    //     currency
    //   );
    //   form.setValue("display_price", formattedPrice, {
    //     shouldValidate: true,
    //   });
    // }
 
      // 追加
      const formattedPrice = await formatCurrency(priceInCorrectUnit, currency);
      form.setValue("original_price", formattedPrice, {
        shouldValidate: true,
      });
      if (!form.getValues("display_price")) {
        form.setValue("display_price", formattedPrice, {
          shouldValidate: true,
        });
      }
 
  // ... その他のコード ...
 
  }
 
// 7. Stripe統合セクションにフォーム項目を追加
  <FormField
    control={form.control}
    name="stripe_coupon_id"
    render={({ field }) => (
      /* ソースコードを直接確認し、完全なコンポーネントをコピーしてください */
    )}
  />
  
  {watchStripeCouponId && (
    <FormField
      control={form.control}
      name="enable_manual_input_coupon"
      render={({ field }) => (
        /* ソースコードを直接確認し、完全なコンポーネントをコピーしてください */
      )}
    />
  )}

ステップ6:クーポンを取得する新しいAPIを追加

app/api/admin/stripe/coupons/route.ts
// ソースコードを直接確認し、完全なコードをコピーしてください

ステップ7:購入ボタンを修正して自動クーポン渡し機能を統合

components/home/PricingCTA.tsx
  // 1. handleCheckoutを修正してクーポン機能をサポート
  const handleCheckout = async (applyCoupon = true) => {
    const stripePriceId = plan.stripe_price_id ?? null;
    if (!stripePriceId) {
      toast.error("Price ID is missing for this plan.");
      return;
    }
 
    const couponCode = plan.stripe_coupon_id;
 
    try {
      const toltReferral = (window as any).tolt_referral;
 
      const requestBody: {
        priceId: string;
        couponCode?: string;
        referral?: string;
      } = {
        priceId: stripePriceId,
      };
 
      if (applyCoupon && couponCode) {
        requestBody.couponCode = couponCode;
      }
 
      if (toltReferral) {
        requestBody.referral = toltReferral;
      }
 
      const response = await fetch("/api/stripe/checkout-session", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Accept-Language": (locale || DEFAULT_LOCALE) as string,
        },
        body: JSON.stringify(requestBody),
      });
      
      // ... その他のコード ...
    } catch (error) {
      // ... その他のコード ...
    }
  }
 
  // 2. ボタンを修正してクーポン機能をサポート
  return (
    <div>
      <Button
        asChild={!!plan.button_link}
        disabled={isLoading}
        // classNameを修正
        className={`w-full flex items-center justify-center gap-2 text-white py-5 font-medium ${
          plan.is_highlighted ? highlightedCtaStyle : defaultCtaStyle
        } ${
          plan.stripe_coupon_id && plan.enable_manual_input_coupon
            ? "mb-2"
            : "mb-6"
        }`}
        {...(!plan.button_link && {
          onClick: () => handleCheckout(),
        })}
      >
        
      </Button>
      /* 新しい購入エントリを追加 */
      {plan.stripe_coupon_id && plan.enable_manual_input_coupon && (
        <div className="text-center mb-2">
          <button
            onClick={() => handleCheckout(false)}
            disabled={isLoading}
            className="text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50 underline underline-offset-2"
          >
            I have a different coupon code
          </button>
        </div>
      )}
    </div>
  );

新しいcredit_logsテーブル

この変更は既存のユーザー権限アップグレード/ダウングレード処理メソッドに影響します。コードが1.2.0より前で、すでに有料ユーザーがいる場合は、credit_logsテーブル関連機能を自分で実装する必要があります。製品がまだ公開されていない、または有料ユーザーがまだいない場合は、以下の手順に従ってコードをアップグレードしてください。

credit-history

データベース設計

  1. 元のクレジット控除メソッドを削除:
DROP FUNCTION IF EXISTS public.upsert_and_increment_one_time_credits;
DROP FUNCTION IF EXISTS public.upsert_and_set_subscription_credits;
DROP FUNCTION IF EXISTS public.revoke_credits;
 
DROP FUNCTION IF EXISTS public.deduct_one_time_credits(uuid, integer);
DROP FUNCTION IF EXISTS public.deduct_subscription_credits(uuid, integer);
DROP FUNCTION IF EXISTS public.deduct_credits_priority_subscription(uuid, integer);
DROP FUNCTION IF EXISTS public.deduct_credits_priority_one_time(uuid, integer);
  1. credit_logsテーブルをクレジット変更のトランザクションログとして作成し、すべての変更を記録して完全な監査証跡を形成。支払い時のクレジット記録、機能使用時のクレジット控除、返金時のクレジット回復のための新しいRPCを作成。

ソースコードdata/8、credit_logs.sqlをご確認ください

  1. ローカルSupabaseタイプを更新
supabase gen types typescript --project-id <your-project-id> --schema public > lib/supabase/types.ts
  1. ユーザークレジット使用例ページとメソッドを更新
  • app/[locale]/(protected)/dashboard/credit-usage-example/page.tsx
  • actions/usage/deduct.ts
  1. webhookイベント処理ロジックを更新

この更新はカスタムユーザー権限アップグレード/ダウングレードメソッドに影響します。すでにカスタムメソッドを実装している場合は、同じアプローチに従って機能を改善できます。カスタムメソッドを修正していない場合は、lib/stripe/webhook-handlers.tsを完全に上書きできます。

  1. 「クレジット履歴」ページを作成

新しいファイル:

  • actions/usage/logs.ts
  • app/[locale]/(protected)/dashboard/(user)/credit-history/*
  • i18n/en/CreditHistory.json
  • i18n/zh/CreditHistory.json
  • i18n/ja/CreditHistory.json

修正されたファイル:

  • i18n/request.ts
  messages: {
    CreditHistory: (await import(`./messages/${locale}/CreditHistory.json`)).default,
    // ... その他のコード ...
  }