Skip to content

Commit 5374a61

Browse files
densumeshcdxker
authored andcommitted
feature: add the ability to create and delete shopify plans and have it reflected in trieve
1 parent cb9e584 commit 5374a61

13 files changed

Lines changed: 226 additions & 41 deletions

File tree

clients/trieve-shopify-extension/app/auth.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { authenticate } from "app/shopify.server";
33
import { StrongTrieveKey, TrieveKey } from "./types";
44
import { TrieveSDK } from "trieve-ts-sdk";
55
import { getTrieveBaseUrlEnv } from "./env.server";
6-
6+
import { JwtPayload } from "jsonwebtoken";
77
export const validateTrieveAuth = async <S extends boolean = true>(
88
request: LoaderFunctionArgs["request"],
99
strict: S = true as S,
@@ -49,6 +49,49 @@ export const validateTrieveAuth = async <S extends boolean = true>(
4949
} as S extends true ? StrongTrieveKey : TrieveKey;
5050
};
5151

52+
export const validateTrieveAuthWehbook = async <S extends boolean = true>(
53+
shop: string,
54+
strict: S = true as S,
55+
): Promise<S extends true ? StrongTrieveKey : TrieveKey> => {
56+
const key = await prisma.apiKey.findFirst({
57+
where: {
58+
shop: `https://${shop}`,
59+
},
60+
});
61+
62+
if (!key) {
63+
throw new Response(
64+
JSON.stringify({ message: "No key matching the current user" }),
65+
{
66+
headers: {
67+
"Content-Type": "application/json; charset=utf-8",
68+
},
69+
status: 401,
70+
},
71+
);
72+
}
73+
74+
if (strict && !key.currentDatasetId) {
75+
throw new Response(
76+
JSON.stringify({ message: "No dataset selected" }),
77+
{
78+
headers: {
79+
"Content-Type": "application/json; charset=utf-8",
80+
},
81+
status: 401,
82+
},
83+
);
84+
}
85+
86+
return {
87+
id: key.id,
88+
key: key.key,
89+
organizationId: key.organizationId,
90+
currentDatasetId: key.currentDatasetId,
91+
userId: key.userId,
92+
} as S extends true ? StrongTrieveKey : TrieveKey;
93+
};
94+
5295
export const sdkFromKey = (key: TrieveKey): TrieveSDK => {
5396
const trieve = new TrieveSDK({
5497
baseUrl: getTrieveBaseUrlEnv(),

clients/trieve-shopify-extension/app/routes/app._dashboard._index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,6 @@ export default function Dashboard() {
142142
);
143143
}
144144

145-
const navigate = useNavigate();
146-
147145
return (
148146
<>
149147
<Modal open={showCancelModal} onClose={() => { setShowCancelModal(false) }} title="Cancel Subscription">

clients/trieve-shopify-extension/app/routes/app._dashboard.chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default function ChatAnalyticsPage() {
4747
granularity={granularity}
4848
direct={false}
4949
/>
50-
<ChatUserJourneyFunnel filters={filters} />
50+
{/* <ChatUserJourneyFunnel filters={filters} /> */}
5151
<TopicCTRRate filters={filters} granularity={granularity} />
5252
<ChatAverageRating filters={filters} granularity={granularity} />
5353
</div>

clients/trieve-shopify-extension/app/routes/webhooks.app.app_subscriptions.update.tsx

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,23 @@ import { authenticate } from "../shopify.server";
33
import { useAppBridge } from "@shopify/app-bridge-react";
44
import { ShopifyPlanChangePayload } from "trieve-ts-sdk";
55
import db from "../db.server";
6+
import { useTrieve } from "app/context/trieveContext";
7+
import { sdkFromKey, validateTrieveAuthWehbook } from "app/auth";
8+
9+
async function hashString(str: string) {
10+
const textEncoder = new TextEncoder();
11+
const data = textEncoder.encode(str);
12+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
13+
const hashArray = Array.from(new Uint8Array(hashBuffer));
14+
const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
15+
return hashHex;
16+
}
617

718
export const action = async ({ request }: ActionFunctionArgs) => {
8-
const { shop, payload, topic } = await authenticate.webhook(request);
19+
const { admin, shop, payload, topic } = await authenticate.webhook(request);
20+
const key = await validateTrieveAuthWehbook(shop);
21+
const trieve = sdkFromKey(key);
22+
923

1024
console.log(`Received ${topic} webhook for ${shop}`);
1125
const organization_id = await db.apiKey.findFirst({
@@ -17,19 +31,31 @@ export const action = async ({ request }: ActionFunctionArgs) => {
1731
return new Response("Organization not found", { status: 404 });
1832
}
1933

34+
const data = await admin?.graphql(
35+
`
36+
query {
37+
currentAppInstallation {
38+
activeSubscriptions {
39+
currentPeriodEnd
40+
}
41+
}
42+
}
43+
`
44+
)
45+
2046
const trievePayload: ShopifyPlanChangePayload = {
2147
organization_id: organization_id?.organizationId,
22-
session_token: process.env.SHOPIFY_SECRET_KEY || "",
48+
idempotency_key: await hashString(`${payload.app_subscription.updated_at}-${payload.app_subscription.admin_graphql_api_id}`),
2349
shopify_plan: {
24-
name: payload.app_subscription.name,
25-
handle: payload.app_subscription.name,
50+
handle: payload.app_subscription.name.replace("\n", "").replace(/ /g, "-").toLowerCase(),
2651
status: payload.app_subscription.status,
52+
current_period_end: (await data?.json())?.data.currentAppInstallation.activeSubscriptions?.[0]?.currentPeriodEnd ?? undefined,
2753
},
2854
};
2955

30-
console.log(
31-
trievePayload
32-
)
56+
console.log(trievePayload)
57+
58+
await trieve.handleShopifyPlanChange(trievePayload, process.env.SHOPIFY_SECRET_KEY || "");
3359

3460
return new Response();
3561
};

clients/ts-sdk/openapi.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19077,15 +19077,15 @@
1907719077
"ShopifyPlan": {
1907819078
"type": "object",
1907919079
"required": [
19080-
"name",
1908119080
"handle",
1908219081
"status"
1908319082
],
1908419083
"properties": {
19085-
"handle": {
19086-
"type": "string"
19084+
"current_period_end": {
19085+
"type": "string",
19086+
"nullable": true
1908719087
},
19088-
"name": {
19088+
"handle": {
1908919089
"type": "string"
1909019090
},
1909119091
"status": {
@@ -19097,17 +19097,17 @@
1909719097
"type": "object",
1909819098
"required": [
1909919099
"organization_id",
19100-
"session_token",
19100+
"idempotency_key",
1910119101
"shopify_plan"
1910219102
],
1910319103
"properties": {
19104+
"idempotency_key": {
19105+
"type": "string"
19106+
},
1910419107
"organization_id": {
1910519108
"type": "string",
1910619109
"format": "uuid"
1910719110
},
19108-
"session_token": {
19109-
"type": "string"
19110-
},
1911119111
"shopify_plan": {
1911219112
"$ref": "#/components/schemas/ShopifyPlan"
1911319113
}

clients/ts-sdk/src/fetch-client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ export class TrieveFetchClient {
142142
headers["TR-Organization"] = value;
143143
} else if (key === "xApiVersion" && typeof value === "string") {
144144
headers["X-API-VERSION"] = value;
145+
} else if (key === "xShopifyAuthorization" && typeof value === "string") {
146+
headers["X-Shopify-Authorization"] = value;
145147
}
146148
// Check if the key is in the path as path params
147149
const snakedKey = camelcaseToSnakeCase(key);

clients/ts-sdk/src/functions/stripe/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import { TrieveSDK } from "../../sdk";
9-
import { StripePlan } from "../../types.gen";
9+
import { ShopifyPlanChangePayload, StripePlan } from "../../types.gen";
1010

1111
export async function getStripePlans(
1212
/** @hidden */
@@ -35,3 +35,16 @@ export async function startStripeCheckout(
3535
}
3636
);
3737
}
38+
39+
40+
export async function handleShopifyPlanChange(
41+
/** @hidden */
42+
this: TrieveSDK,
43+
payload: ShopifyPlanChangePayload,
44+
shopifySecretKey: string
45+
) {
46+
return this.trieve.fetch<"eject">("/api/shopify/plan_change", "post", {
47+
data: payload,
48+
xShopifyAuthorization: shopifySecretKey,
49+
});
50+
}

clients/ts-sdk/src/types.gen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3749,14 +3749,14 @@ export type ShopifyCustomerEvent = {
37493749
};
37503750

37513751
export type ShopifyPlan = {
3752+
current_period_end?: (string) | null;
37523753
handle: string;
3753-
name: string;
37543754
status: string;
37553755
};
37563756

37573757
export type ShopifyPlanChangePayload = {
3758+
idempotency_key: string;
37583759
organization_id: string;
3759-
session_token: string;
37603760
shopify_plan: ShopifyPlan;
37613761
};
37623762

server/src/data/models.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4013,7 +4013,16 @@ impl StripeUsageBasedPlan {
40134013
}
40144014

40154015
#[derive(
4016-
Debug, Serialize, Deserialize, Selectable, Clone, Queryable, Insertable, ValidGrouping, ToSchema,
4016+
Debug,
4017+
Serialize,
4018+
Deserialize,
4019+
Selectable,
4020+
Clone,
4021+
Queryable,
4022+
Insertable,
4023+
ValidGrouping,
4024+
ToSchema,
4025+
AsChangeset,
40174026
)]
40184027
#[schema(example=json!({
40194028
"id": "e3e3e3e3-e3e3-e3e3-e3e3-e3e3e3e3e3e3",

server/src/handlers/payment_handler.rs

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use crate::{
2727
},
2828
};
2929
use actix_web::{web, HttpRequest, HttpResponse};
30+
use dateparser::DateTimeUtc;
3031
use serde::{Deserialize, Serialize};
3132
use stripe::{EventObject, EventType, Object, Webhook};
3233
use utoipa::ToSchema;
@@ -205,6 +206,7 @@ pub async fn webhook(
205206
subscription_stripe_id,
206207
plan_id,
207208
organization_id,
209+
None,
208210
pool.clone(),
209211
)
210212
.await?;
@@ -480,6 +482,7 @@ pub async fn update_subscription_plan(
480482
current_trieve_subscription.stripe_subscription_id(),
481483
flat_plan.id,
482484
current_trieve_subscription.organization_id(),
485+
current_trieve_subscription.current_period_end(),
483486
pool.clone(),
484487
)
485488
.await?;
@@ -722,20 +725,15 @@ pub async fn estimate_bill_from_range(
722725
#[derive(Debug, Serialize, Deserialize, ToSchema)]
723726
pub struct ShopifyPlanChangePayload {
724727
pub organization_id: uuid::Uuid,
725-
pub session_token: String,
728+
pub idempotency_key: String,
726729
pub shopify_plan: ShopifyPlan,
727730
}
728731

729732
#[derive(Debug, Serialize, Deserialize, ToSchema)]
730733
pub struct ShopifyPlan {
731-
pub name: String,
732734
pub handle: String,
733735
pub status: String,
734-
}
735-
736-
#[derive(Debug, Serialize, Deserialize, ToSchema)]
737-
pub struct ShopifyClaims {
738-
pub dest: String,
736+
pub current_period_end: Option<String>,
739737
}
740738

741739
#[utoipa::path(
@@ -749,10 +747,22 @@ pub struct ShopifyClaims {
749747
),
750748
)]
751749
pub async fn handle_shopify_plan_change(
750+
req: HttpRequest,
752751
payload: web::Json<ShopifyPlanChangePayload>,
753752
pool: web::Data<Pool>,
754753
) -> Result<HttpResponse, actix_web::Error> {
755-
if payload.session_token != get_env!("SHOPIFY_SECRET_KEY", "SHOPIFY_SECRET_KEY must be set") {
754+
let access_key = req
755+
.headers()
756+
.get("X-Shopify-Authorization")
757+
.ok_or(ServiceError::BadRequest(
758+
"X-Shopify-Authorization header is required".to_string(),
759+
))?
760+
.to_str()
761+
.map_err(|_| {
762+
ServiceError::BadRequest("Failed to parse X-Shopify-Authorization header".to_string())
763+
})?;
764+
765+
if access_key != get_env!("SHOPIFY_SECRET_KEY", "SHOPIFY_SECRET_KEY must be set") {
756766
return Err(ServiceError::BadRequest("Invalid session token".to_string()).into());
757767
}
758768

@@ -766,6 +776,11 @@ pub async fn handle_shopify_plan_change(
766776
.map_err(|e| ServiceError::BadRequest(e.to_string()))?;
767777

768778
if let Some(organization_plan) = organization_plan {
779+
if organization_plan.stripe_id == payload.idempotency_key {
780+
// No changes
781+
return Ok(HttpResponse::NoContent().finish());
782+
}
783+
769784
if organization_plan.plan_id == plan.id
770785
&& payload.shopify_plan.status.to_lowercase() == "active"
771786
{
@@ -786,19 +801,33 @@ pub async fn handle_shopify_plan_change(
786801
{
787802
// Create a new plan
788803
create_stripe_subscription_query(
789-
plan.stripe_id,
804+
payload.idempotency_key.clone(),
790805
plan.id,
791806
payload.organization_id,
807+
payload.shopify_plan.current_period_end.clone().map(|s| {
808+
s.parse::<DateTimeUtc>()
809+
.unwrap()
810+
.0
811+
.with_timezone(&chrono::Utc)
812+
.naive_utc()
813+
}),
792814
pool.clone(),
793815
)
794816
.await?;
795817
}
796818
} else if payload.shopify_plan.status.to_lowercase() == "active" {
797819
// Create a new plan
798820
create_stripe_subscription_query(
799-
plan.stripe_id,
821+
payload.idempotency_key.clone(),
800822
plan.id,
801823
payload.organization_id,
824+
payload.shopify_plan.current_period_end.clone().map(|s| {
825+
s.parse::<DateTimeUtc>()
826+
.unwrap()
827+
.0
828+
.with_timezone(&chrono::Utc)
829+
.naive_utc()
830+
}),
802831
pool.clone(),
803832
)
804833
.await?;

0 commit comments

Comments
 (0)