Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add free trial pricing table to emails when appropriate #452

Merged
merged 5 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,16 @@ MAILSLURP_TIMEOUT = '120000'
# Stripe
# these values are from the Stripe test environment
STRIPE_PRICING_TABLE_ID = 'prctbl_1NzhdvF6A5ufQX5vKNZuRhie'
STRIPE_FREE_TRIAL_PRICING_TABLE_ID = 'prctbl_1QHa8sF6A5ufQX5vJ8SUZUjq'
STRIPE_PUBLISHABLE_KEY = 'pk_test_51LO87hF6A5ufQX5viNsPTbuErzfavdrEFoBuaJJPfoIhzQXdOUdefwL70YewaXA32ZrSRbK4U4fqebC7SVtyeNcz00qmgNgueC'
# this is used in tests and should always be set to the test env secret key
STRIPE_TEST_SECRET_KEY = ''
STRIPE_BILLING_METER_ID = ''
STRIPE_BILLING_METER_EVENT_NAME = ''

# Feature flags
REQUIRE_PAYMENT_PLAN = 'true'

# Referrals

REFERRALS_ENDPOINT = 'https://staging.referrals.storacha.network'
2 changes: 2 additions & 0 deletions stacks/upload-api-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
const git = getGitInfo()
const ucanInvocationPostbasicAuth = new Config.Secret(stack, 'UCAN_INVOCATION_POST_BASIC_AUTH')

const apis = (customDomains ?? [undefined]).map((customDomain, idx) => {

Check warning on line 60 in stacks/upload-api-stack.js

View workflow job for this annotation

GitHub Actions / Test

Arrow function has a complexity of 22. Maximum allowed is 20
const hostedZone = customDomain?.hostedZone
// the first customDomain will be web3.storage, and we don't want the apiId for that domain to have a second part, see PR of this change for context
const apiId = [`http-gateway`, idx > 0 ? hostedZone?.replaceAll('.', '_') : '']
Expand Down Expand Up @@ -141,9 +141,11 @@
REQUIRE_PAYMENT_PLAN: process.env.REQUIRE_PAYMENT_PLAN ?? '',
UPLOAD_API_DID: process.env.UPLOAD_API_DID ?? '',
STRIPE_PRICING_TABLE_ID: process.env.STRIPE_PRICING_TABLE_ID ?? '',
STRIPE_FREE_TRIAL_PRICING_TABLE_ID: process.env.STRIPE_FREE_TRIAL_PRICING_TABLE_ID ?? '',
STRIPE_PUBLISHABLE_KEY: process.env.STRIPE_PUBLISHABLE_KEY ?? '',
DEAL_TRACKER_DID: process.env.DEAL_TRACKER_DID ?? '',
DEAL_TRACKER_URL: process.env.DEAL_TRACKER_URL ?? '',
REFERRALS_ENDPOINT: process.env.REFERRALS_ENDPOINT ?? '',
CONTENT_CLAIMS_DID,
CONTENT_CLAIMS_URL,
CONTENT_CLAIMS_PROOF,
Expand Down
29 changes: 19 additions & 10 deletions upload-api/functions/validate-email.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createWorkflowStore } from '../buckets/workflow-store.js'
import { createSubscriptionTable } from '../tables/subscription.js'
import { createConsumerTable } from '../tables/consumer.js'
import { createRevocationsTable } from '../stores/revocations.js'
import { createReferralStore } from '../stores/referrals.js'
import * as AgentStore from '../stores/agent.js'
import { useProvisionStore } from '../stores/provisions.js'
// eslint-disable-next-line import/extensions
Expand Down Expand Up @@ -90,7 +91,9 @@ function createAuthorizeContext() {
UPLOAD_API_DID = '',
PROVIDERS = '',
STRIPE_PRICING_TABLE_ID = '',
STRIPE_FREE_TRIAL_PRICING_TABLE_ID = '',
STRIPE_PUBLISHABLE_KEY = '',
REFERRALS_ENDPOINT = '',
UCAN_LOG_STREAM_NAME = '',
SST_STAGE = '',
// set for testing
Expand Down Expand Up @@ -122,6 +125,7 @@ function createAuthorizeContext() {
{ region: AWS_REGION },
{ tableName: CUSTOMER_TABLE_NAME }
)
const referralStore = createReferralStore({ endpoint: REFERRALS_ENDPOINT })
const spaceMetricsTable = createSpaceMetricsTable(
AWS_REGION,
SPACE_METRICS_TABLE_NAME
Expand Down Expand Up @@ -171,8 +175,10 @@ function createAuthorizeContext() {
),
rateLimitsStorage: createRateLimitTable(AWS_REGION, RATE_LIMIT_TABLE_NAME),
customerStore,
referralStore,
agentStore,
stripePricingTableId: STRIPE_PRICING_TABLE_ID,
stripeFreeTrialPricingTableId: STRIPE_FREE_TRIAL_PRICING_TABLE_ID,
stripePublishableKey: STRIPE_PUBLISHABLE_KEY,
}
}
Expand All @@ -183,7 +189,6 @@ function createAuthorizeContext() {
* @param {import('aws-lambda').APIGatewayProxyEventV2} request
*/
export async function validateEmailPost(request) {
console.log("VALIDATING EMAIL")
const encodedUcan = request.queryStringParameters?.ucan
if (!encodedUcan) {
return toLambdaResponse(
Expand All @@ -192,13 +197,9 @@ export async function validateEmailPost(request) {
)
)
}
console.log("CREATING CONTEXT")

const context = createAuthorizeContext()
console.log("AUTHORIZING")

const authorizeResult = await authorize(encodedUcan, context)
console.log("AUTHORIZED", JSON.stringify(authorizeResult))

if (authorizeResult.error) {
console.error(authorizeResult.error)
Expand All @@ -215,21 +216,28 @@ export async function validateEmailPost(request) {
}

const { email, audience, ucan } = authorizeResult.ok
console.log("CHECKING PLAN")

const planCheckResult = await context.customerStore.get({
customer: DidMailto.fromEmail(email),
})
let isReferred = false
try {
// if we can find a referral code for this user, offer them a free trial
if ((await context.referralStore.getReferredBy(email)).refcode) {
isReferred = true
}
} catch (e){
// if we fail here, log the error and move on
console.warn('encountered an error checking the referrals service, please see the error logs for more information')
console.error(e)
}
let stripePricingTableId
let stripePublishableKey
console.log("CHECKED PLAN", JSON.stringify(planCheckResult))

if (!planCheckResult.ok?.product) {
stripePricingTableId = context.stripePricingTableId
stripePublishableKey = context.stripePublishableKey
stripePricingTableId = isReferred ? context.stripeFreeTrialPricingTableId : context.stripePricingTableId
}


return toLambdaResponse(
new html.HtmlResponse(
(
Expand All @@ -239,6 +247,7 @@ export async function validateEmailPost(request) {
ucan={ucan}
stripePricingTableId={stripePricingTableId}
stripePublishableKey={stripePublishableKey}
isReferred={isReferred}
/>
)
)
Expand Down
25 changes: 21 additions & 4 deletions upload-api/html-storacha/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,15 @@ export const PendingValidateEmail = ({ autoApprove }) => (
* @param {string} props.audience
* @param {string} [props.stripePricingTableId]
* @param {string} [props.stripePublishableKey]
* @param {boolean} [props.isReferred]
*/
export const ValidateEmail = ({
ucan,
email,
audience,
stripePricingTableId,
stripePublishableKey,
isReferred
}) => {
const showPricingTable = stripePricingTableId && stripePublishableKey
return (
Expand All @@ -220,9 +222,24 @@ export const ValidateEmail = ({
</div>
{showPricingTable && (
<div class="box">
<p>
In order to upload data you need to sign up for a billing plan:
</p>
{isReferred ? (
<>
<p>
Congratulations! You are eligible for a free trial of our Lite or Business subscriptions. That means
we won&apos;t charge you anything today.
If you choose a Lite plan, you will get two months for free! If you choose Business, you will get one month for free!
We do need you to provide a valid credit card before we can start your
trial - pick a plan below and complete the checkout flow to get started!
</p>
<p>
Please note that after your free trial ends, you will be charged 10 USD per month for Lite or 100 USD per month for Business tier.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Is it safe to write these values here literally? They won't update if the pricing changes.

</p>
</>
) : (
<p>
In order to upload data you need to sign up for a billing plan:
</p>
)}
<stripe-pricing-table
pricing-table-id={stripePricingTableId}
publishable-key={stripePublishableKey}
Expand All @@ -233,7 +250,7 @@ export const ValidateEmail = ({
<div class="box">
<p>
By registering with Storacha you agree to the Storacha{' '}
<a href="https://web3.storage/docs/terms/">Terms of Service</a>.
<a href="https://docs.storacha.network/terms/">Terms of Service</a>.
</p>
</div>
<details class="box">
Expand Down
15 changes: 15 additions & 0 deletions upload-api/stores/referrals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Abstraction layer for referrals.
*
* @param {object} [options]
* @param {string} [options.endpoint]
* @returns {import("../types").ReferralsStore}
*/
export function createReferralStore(options = {}) {
return {
async getReferredBy(email) {
const result = await fetch(`${options.endpoint}/referredby/${email}`)
return await result.json()
}
}
}
8 changes: 8 additions & 0 deletions upload-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,12 @@ export interface BillingProvider {
createAdminSession: (customer: AccountDID, returnURL: string) => Promise<Result<PlanCreateAdminSessionSuccess, PlanCreateAdminSessionFailure>>
}

export interface Referral {
refcode: string
}

export interface ReferralsStore {
getReferredBy: (email: string) => Promise<Referral>
}

export {}
Loading