Skip to content

Commit

Permalink
fetch theme from stripe for all pages
Browse files Browse the repository at this point in the history
  • Loading branch information
kasparkallas committed Nov 2, 2023
1 parent 2a62be5 commit 38331a6
Show file tree
Hide file tree
Showing 17 changed files with 464 additions and 379 deletions.
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"clean": "npm-run-all -s clean:*",
"clean:dist": "rimraf \"./dist\"",
"clean:node-modules": "rimraf \"./node_modules\"",
"generate": "npm-run-all -s generate:*",
"generate:accounting-openapi-client": "pnpm exec openapi-typescript https://accounting.superfluid.dev/static/api-docs.yaml -o ./src/super-token-accounting/client/types.d.ts"
},
"jest": {
Expand Down
4 changes: 1 addition & 3 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,4 @@ const registerBullModule = () =>
controllers: [],
providers: [],
})
export class AppModule {

}
export class AppModule {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { tierASuperTokenList } from "@superfluid-finance/tokenlist"
import { tierASuperTokenList } from '@superfluid-finance/tokenlist';

import Stripe from "stripe";
import Stripe from 'stripe';

export const CUSTOMER_EMAIL = 'stripe@superfluid.finance'; // This is always the key for finding the customers.

Expand All @@ -19,30 +19,29 @@ export const DEFAULT_LOOK_AND_FEEL_CUSTOMER = {
} as const satisfies Stripe.CustomerCreateParams;

const liveCurrencyTokenSymbols = {
usd: ["USDCx", "USDTx", "DAIx", "cUSDx", "G$", "mUSDx"],
eur: ["cEURx", "EUROex", "EURSx", "agEURx", "jEURx", "EURex"],
cad: ["jCADx"],
bgn: ["jBGNx"],
chf: ["jXOFx"],
php: ["jPHPx"],
xaf: ["jXAFx"],
sgd: ["jSGDx"],
jpy: ["JPYCx"],
}
usd: ['USDCx', 'USDTx', 'DAIx', 'cUSDx', 'G$', 'mUSDx'],
eur: ['cEURx', 'EUROex', 'EURSx', 'agEURx', 'jEURx', 'EURex'],
cad: ['jCADx'],
bgn: ['jBGNx'],
chf: ['jXOFx'],
php: ['jPHPx'],
xaf: ['jXAFx'],
sgd: ['jSGDx'],
jpy: ['JPYCx'],
};

// This is the default customer for on-chain settings.
export const createDefaultBlockChainCustomer = (testMode: boolean): Stripe.CustomerCreateParams => {

return ({
return {
email: CUSTOMER_EMAIL,
name: BLOCKCHAIN_CUSTOMER_NAME,
description: 'Auto-generated fake customer for Superfluid integration.',
metadata: {
chain_5_usd_token: '0x8ae68021f6170e5a766be613cea0d75236ecca9a',
chain_5_receiver: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
default_receiver: ''
default_receiver: '',
},
});
};
};

// This is the default customer for on-chain settings.
Expand Down Expand Up @@ -85,4 +84,4 @@ export const FIRST_TIME_EXAMPLE_PRODUCT: Stripe.ProductCreateParams = {
// metadata: {
// superfluid: `The value here does not matter. When "superfluid" metadata key is specified then it is valid for the Superfluid integration.`
// }
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,111 @@ import { Injectable, Logger } from '@nestjs/common';
import { DEFAULT_PAGING } from 'src/stripe-module-config';
import Stripe from 'stripe';
import { Address, ChainId, StripeCurrencyKey } from './basic-types';
import { isAddress } from 'viem';
import { BLOCKCHAIN_CUSTOMER_NAME, CUSTOMER_EMAIL, DEFAULT_LOOK_AND_FEEL_CUSTOMER, FIRST_TIME_EXAMPLE_PRODUCT, LOOK_AND_FEEL_CUSTOMER_NAME, createDefaultBlockChainCustomer } from './stripe-entities';
import { Block, isAddress } from 'viem';
import {
BLOCKCHAIN_CUSTOMER_NAME,
CUSTOMER_EMAIL,
DEFAULT_LOOK_AND_FEEL_CUSTOMER,
FIRST_TIME_EXAMPLE_PRODUCT,
LOOK_AND_FEEL_CUSTOMER_NAME,
createDefaultBlockChainCustomer,
} from './stripe-entities';
import { ConfigService } from '@nestjs/config';

export type IntegrationConfig = {
export type LookAndFeelConfig = {
theme: any; // TODO(KK): any
};

export type BlockchainConfig = {
version: string;
chains: ReadonlyArray<ChainConfig>;
theme: any; // TODO(KK): any
// lookAndFeel: Record<string, any>;
};

export type CompleteConfig = LookAndFeelConfig & BlockchainConfig;

interface GlobalConfigCustomerManager {
loadOrInitializeConfig(): Promise<IntegrationConfig>;
loadOrInitializeCompleteConfig(): Promise<CompleteConfig>;
loadOrInitializeLookAndFeelConfig(): Promise<LookAndFeelConfig>;
loadOrInitializeBlockchainConfig(): Promise<BlockchainConfig>;
}

@Injectable()
export class SuperfluidStripeConfigService implements GlobalConfigCustomerManager {
constructor(@InjectStripeClient() private readonly stripeClient: Stripe, private readonly configService: ConfigService) {}
private readonly stripeTestMode;

async loadOrInitializeConfig(): Promise<IntegrationConfig> {
const stripeTestMode = this.configService.get("STRIPE_TEST_MODE") === "true";
constructor(
@InjectStripeClient() private readonly stripeClient: Stripe,
private readonly configService: ConfigService,
) {
this.stripeTestMode = this.configService.get('STRIPE_TEST_MODE') === 'true';
}

// TODO: caching
// TODO: use better constants
async loadOrInitializeLookAndFeelConfig(): Promise<LookAndFeelConfig> {
const { lookAndFeelCustomer: lookAndFeelCustomer_ } = await this.loadCustomers();
const lookAndFeelCustomer =
lookAndFeelCustomer_ ??
(await this.stripeClient.customers.create(DEFAULT_LOOK_AND_FEEL_CUSTOMER));

const customers = await this.stripeClient.customers
.list({
email: CUSTOMER_EMAIL,
})
.autoPagingToArray(DEFAULT_PAGING);
// TODO: use Zod for validation?
// TODO: get rid of any
const theme = JSON.parse(lookAndFeelCustomer.metadata.theme);

return {
theme,
};
}

async loadOrInitializeBlockchainConfig(): Promise<BlockchainConfig> {
const { blockchainCustomer: blockchainCustomer_ } = await this.loadCustomers();
const blockchainCustomer =
blockchainCustomer_ ??
(await this.stripeClient.customers.create(
createDefaultBlockChainCustomer(this.stripeTestMode),
));

const chainConfigs = mapBlockchainCustomerMetadataIntoChainConfigs(blockchainCustomer.metadata);

return {
version: '1.0.0',
chains: chainConfigs,
};
}

let blockchainCustomer = customers.find((x) => x.name === BLOCKCHAIN_CUSTOMER_NAME);
let lookAndFeelCustomer = customers.find((x) => x.name === LOOK_AND_FEEL_CUSTOMER_NAME);
async loadOrInitializeCompleteConfig(): Promise<CompleteConfig> {
const { lookAndFeelCustomer, blockchainCustomer } = await this.loadCustomers();

if (stripeTestMode) {
const isFirstTimeUsage = !blockchainCustomer && !lookAndFeelCustomer;
const isFirstTimeUsage = !lookAndFeelCustomer && !blockchainCustomer;
if (this.stripeTestMode) {
if (isFirstTimeUsage) {
await this.stripeClient.products.create(FIRST_TIME_EXAMPLE_PRODUCT);
}
}

if (!blockchainCustomer) {
blockchainCustomer = await this.stripeClient.customers.create(createDefaultBlockChainCustomer(stripeTestMode));
}
const lookAndFeelConfig = await this.loadOrInitializeLookAndFeelConfig();
const blockchainConfig = await this.loadOrInitializeBlockchainConfig();

if (!lookAndFeelCustomer) {
lookAndFeelCustomer = await this.stripeClient.customers.create(
DEFAULT_LOOK_AND_FEEL_CUSTOMER,
);
}
return {
...lookAndFeelConfig,
...blockchainConfig,
};
}

const chainConfigs = mapBlockchainCustomerMetadataIntoChainConfigs(blockchainCustomer.metadata);
private async loadCustomers(): Promise<{
blockchainCustomer: Stripe.Customer | undefined;
lookAndFeelCustomer: Stripe.Customer | undefined;
}> {
// TODO: cache

// TODO: use Zod for validation?
// TODO: get rid of any
let theme: any;
try {
theme = JSON.parse(lookAndFeelCustomer.metadata['theme']);
} catch (e) {
logger.error(e);
}
const customers = await this.stripeClient.customers
.list({
email: CUSTOMER_EMAIL,
})
.autoPagingToArray(DEFAULT_PAGING);

const mappedResult: IntegrationConfig = {
version: '1.0.0',
chains: chainConfigs,
theme,
return {
blockchainCustomer: customers.find((x) => x.name === BLOCKCHAIN_CUSTOMER_NAME),
lookAndFeelCustomer: customers.find((x) => x.name === LOOK_AND_FEEL_CUSTOMER_NAME),
};

return mappedResult;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { WidgetProps } from '@superfluid-finance/widget';
import Stripe from 'stripe';
import { SuperfluidStripeConverterService } from './superfluid-stripe-converter.service';
import { DEFAULT_PAGING } from 'src/stripe-module-config';
import { SuperfluidStripeConfigService } from './superfluid-stripe-config/superfluid-stripe-config.service';
import {
LookAndFeelConfig,
SuperfluidStripeConfigService,
} from './superfluid-stripe-config/superfluid-stripe-config.service';

type ProductResponse = {
stripeProduct: Stripe.Product;
Expand All @@ -25,31 +28,31 @@ export class SuperfluidStripeConverterController {
async mapStripeProductToCheckoutWidget(
@Query('product-id') productId: string,
): Promise<ProductResponse> {
const [stripeProduct, stripePrices, integrationConfig] = await Promise.all([
const [stripeProduct, stripePrices, blockchainConfig] = await Promise.all([
this.stripeClient.products.retrieve(productId),
this.stripeClient.prices
.list({
product: productId,
active: true,
})
.autoPagingToArray(DEFAULT_PAGING),
this.superfluidStripeConfigService.loadOrInitializeConfig(),
this.superfluidStripeConfigService.loadOrInitializeBlockchainConfig(),
]);

// check eligibility somewhere?

const wigetConfig = await this.superfluidStripeConverterService.mapStripeProductToWidgetConfig({
integrationConfig: integrationConfig,
preloadedBlockchainConfig: blockchainConfig,
product: stripeProduct,
prices: stripePrices,
});

return { ...wigetConfig, stripeProduct: stripeProduct };
return { ...wigetConfig, stripeProduct };
}

@Get('products')
async products(): Promise<ProductResponse[]> {
const [stripeProducts, stripePrices, integrationConfig] = await Promise.all([
const [stripeProducts, stripePrices, blockchainConfig] = await Promise.all([
this.stripeClient.products
.list({
active: true,
Expand All @@ -60,7 +63,7 @@ export class SuperfluidStripeConverterController {
active: true,
})
.autoPagingToArray(DEFAULT_PAGING),
this.superfluidStripeConfigService.loadOrInitializeConfig(),
this.superfluidStripeConfigService.loadOrInitializeCompleteConfig(),
]);

// check eligibility somewhere?
Expand All @@ -69,18 +72,27 @@ export class SuperfluidStripeConverterController {
stripeProducts.map(async (stripeProduct) => {
const pricesForProduct = stripePrices.filter((price) => price.product === stripeProduct.id);

const config = await this.superfluidStripeConverterService.mapStripeProductToWidgetConfig({
product: stripeProduct,
prices: pricesForProduct,
integrationConfig,
});
const widgetConfig =
await this.superfluidStripeConverterService.mapStripeProductToWidgetConfig({
preloadedBlockchainConfig: blockchainConfig,
product: stripeProduct,
prices: pricesForProduct,
});

return { ...config, stripeProduct };
return { ...widgetConfig, stripeProduct };
}),
);

return results;
}

// TODO(KK): cache aggressively
@Get('look-and-feel')
async lookAndFeel(): Promise<LookAndFeelConfig> {
const lookAndFeelConfig =
await this.superfluidStripeConfigService.loadOrInitializeLookAndFeelConfig();
return lookAndFeelConfig;
}
}

const logger = new Logger(SuperfluidStripeConverterController.name);
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import { SuperfluidStripeConfigService } from './superfluid-stripe-config/superf
exports: [SuperfluidStripeConverterService],
})
export class SuperfluidStripeConverterModule implements OnModuleInit {
constructor(
private readonly configService: SuperfluidStripeConfigService,
) {}
constructor(private readonly configService: SuperfluidStripeConfigService) {}

async onModuleInit() {
// Initialize the Superfluid-Stripe integration global configuration objects (e.g. the "fake" Stripe Customers)
await this.configService.loadOrInitializeConfig();
await this.configService.loadOrInitializeCompleteConfig();
}
}
Loading

0 comments on commit 38331a6

Please sign in to comment.