fix: added fallback for creating org at onboarding if clerk webhook comes late / fails

This commit is contained in:
John Yeo
2025-04-22 22:42:38 +01:00
parent bebafbef2f
commit 5255b61410
15 changed files with 767 additions and 587 deletions

View File

@@ -1 +1 @@
AUTUMN_SECRET_KEY=am_sk_test_RCM3U5M8ptWm6AWhE68hTeVn0sACxX6JE0zpHJi3SV # Example Org
AUTUMN_SECRET_KEY=am_sk_test_8bDtE5xZ3pLO4Pl1yQvpKc91fYAvdNTVZ2vOkR5jNe # Example Org

View File

@@ -12,6 +12,7 @@
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"autumn-js": "^0.0.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.483.0",
@@ -48,6 +49,134 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@better-auth/utils": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"typescript": "^5.8.2",
"uncrypto": "^0.1.3"
}
},
"node_modules/@better-fetch/fetch": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz",
"integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==",
"peer": true
},
"node_modules/@clerk/backend": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-1.29.2.tgz",
"integrity": "sha512-mBvGyHjNj/t2X2y0Dn1V1EgsffhgNH1TJhB98k40dHxVxvosY9QdgCDMtru/Q1uZ7EXNOzYqzcB5FzAKCxFRZw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@clerk/shared": "^3.7.3",
"@clerk/types": "^4.55.0",
"cookie": "1.0.2",
"snakecase-keys": "8.0.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=18.17.0"
},
"peerDependencies": {
"svix": "^1.62.0"
},
"peerDependenciesMeta": {
"svix": {
"optional": true
}
}
},
"node_modules/@clerk/clerk-react": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.29.0.tgz",
"integrity": "sha512-S8wenyl5R/8YmUT1tElQZdmHe/MASM9JDJRKqhy0kVYXdKzjhuwTBgT+m3hXl3zdq/Gwi3hgv7pRnQWpQisutQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@clerk/shared": "^3.7.3",
"@clerk/types": "^4.55.0",
"tslib": "2.8.1"
},
"engines": {
"node": ">=18.17.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-0",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0"
}
},
"node_modules/@clerk/nextjs": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.17.0.tgz",
"integrity": "sha512-ik+UU/cKZgC5pafJJ9oct0BUHvytUqof+r1RjFb71xU1TM7EOeL5XYafVxh+hlgwsE4jmrw+FVYe4DXDcyPeTQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@clerk/backend": "^1.29.2",
"@clerk/clerk-react": "^5.29.0",
"@clerk/shared": "^3.7.3",
"@clerk/types": "^4.55.0",
"server-only": "0.0.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=18.17.0"
},
"peerDependencies": {
"next": "^13.5.7 || ^14.2.25 || ^15.2.3",
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-0",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0"
}
},
"node_modules/@clerk/shared": {
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.7.3.tgz",
"integrity": "sha512-IMpz1bgZZzlxNLyFliE0GrA3NT1Wgidl2CBORkE2S/YUSCH4ge8EPg5tfQq9Ja6d17TfarkzEafrm1b/BHxSWg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@clerk/types": "^4.55.0",
"dequal": "2.0.3",
"glob-to-regexp": "0.4.1",
"js-cookie": "3.0.5",
"std-env": "^3.8.1",
"swr": "^2.3.3"
},
"engines": {
"node": ">=18.17.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-0",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/@clerk/types": {
"version": "4.55.0",
"resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.55.0.tgz",
"integrity": "sha512-MJc/zo9vJOq1mY37RbRjrZ883AKe9gitlYFyVB3R+ChKm0uBl2Mm8R2gkYy1GGKYvqBm27GJooWHxdcM1Yv1eQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "3.1.3"
},
"engines": {
"node": ">=18.17.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
@@ -257,6 +386,13 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@hexagon/base64": {
"version": "1.1.28",
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==",
"license": "MIT",
"peer": true
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -684,6 +820,13 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@levischuck/tiny-cbor": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==",
"license": "MIT",
"peer": true
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz",
@@ -841,6 +984,29 @@
"node": ">= 10"
}
},
"node_modules/@noble/ciphers": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.6.0.tgz",
"integrity": "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"peer": true,
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -889,6 +1055,69 @@
"node": ">=12.4.0"
}
},
"node_modules/@peculiar/asn1-android": {
"version": "2.3.16",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.16.tgz",
"integrity": "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-ecc": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.15.tgz",
"integrity": "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"@peculiar/asn1-x509": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-rsa": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.15.tgz",
"integrity": "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"@peculiar/asn1-x509": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-schema": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz",
"integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==",
"license": "MIT",
"peer": true,
"dependencies": {
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-x509": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.15.tgz",
"integrity": "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
@@ -1392,6 +1621,32 @@
"dev": true,
"license": "MIT"
},
"node_modules/@simplewebauthn/browser": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.0.tgz",
"integrity": "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==",
"license": "MIT",
"peer": true
},
"node_modules/@simplewebauthn/server": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.1.1.tgz",
"integrity": "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@hexagon/base64": "^1.1.27",
"@levischuck/tiny-cbor": "^0.2.2",
"@peculiar/asn1-android": "^2.3.10",
"@peculiar/asn1-ecc": "^2.3.8",
"@peculiar/asn1-rsa": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -2350,6 +2605,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/asn1js": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz",
"integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"pvtsutils": "^1.3.6",
"pvutils": "^1.1.3",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/ast-types-flow": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@@ -2367,6 +2637,18 @@
"node": ">= 0.4"
}
},
"node_modules/autumn-js": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/autumn-js/-/autumn-js-0.0.7.tgz",
"integrity": "sha512-sk+i5aMzB1xPi3N/4PD00k1cS9FlR/0xkZ2mqOyksCD23CcaczmRir45o6zzhNFQ2Q1K1icUwRyljwlW3RHWTQ==",
"license": "MIT",
"peerDependencies": {
"@clerk/nextjs": "^6.16.0",
"better-auth": "^1.2.7",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2410,6 +2692,38 @@
"dev": true,
"license": "MIT"
},
"node_modules/better-auth": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.2.7.tgz",
"integrity": "sha512-2hCB263GSrgetsMUZw8vv9O1e4S4AlYJW3P4e8bX9u3Q3idv4u9BzDFCblpTLuL4YjYovghMCN0vurAsctXOAQ==",
"peer": true,
"dependencies": {
"@better-auth/utils": "0.2.4",
"@better-fetch/fetch": "^1.1.18",
"@noble/ciphers": "^0.6.0",
"@noble/hashes": "^1.6.1",
"@simplewebauthn/browser": "^13.0.0",
"@simplewebauthn/server": "^13.0.0",
"better-call": "^1.0.8",
"defu": "^6.1.4",
"jose": "^5.9.6",
"kysely": "^0.27.6",
"nanostores": "^0.11.3",
"zod": "^3.24.1"
}
},
"node_modules/better-call": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.0.8.tgz",
"integrity": "sha512-/PV8JLqDRUN7JyBPbklVsS/8E4SO3pnf8hbpa8B7xrBrr+BBYpeOAxoqtnsyk/pRs35vNB4MZx8cn9dBuNlLDA==",
"peer": true,
"dependencies": {
"@better-fetch/fetch": "^1.1.4",
"rou3": "^0.5.1",
"set-cookie-parser": "^2.7.1",
"uncrypto": "^0.1.3"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2621,6 +2935,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2640,7 +2964,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -2765,6 +3088,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT",
"peer": true
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@@ -2794,6 +3134,17 @@
"node": ">=0.10.0"
}
},
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
"license": "MIT",
"peer": true,
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3717,6 +4068,13 @@
"node": ">=10.13.0"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"license": "BSD-2-Clause",
"peer": true
},
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -4357,6 +4715,26 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jose": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4436,6 +4814,16 @@
"json-buffer": "3.0.1"
}
},
"node_modules/kysely": {
"version": "0.27.6",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.6.tgz",
"integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -4744,6 +5132,16 @@
"loose-envify": "cli.js"
}
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.0.3"
}
},
"node_modules/lucide-react": {
"version": "0.483.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.483.0.tgz",
@@ -4753,6 +5151,19 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/map-obj": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4835,6 +5246,22 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/nanostores": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.4.tgz",
"integrity": "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -4934,6 +5361,17 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"license": "MIT",
"peer": true,
"dependencies": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5255,6 +5693,26 @@
"node": ">=6"
}
},
"node_modules/pvtsutils": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/pvutils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5473,6 +5931,13 @@
"node": ">=0.10.0"
}
},
"node_modules/rou3": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz",
"integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==",
"license": "MIT",
"peer": true
},
"node_modules/rspack-resolver": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rspack-resolver/-/rspack-resolver-1.2.2.tgz",
@@ -5597,6 +6062,20 @@
"node": ">=10"
}
},
"node_modules/server-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
"license": "MIT",
"peer": true
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT",
"peer": true
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -5795,6 +6274,32 @@
"is-arrayish": "^0.3.1"
}
},
"node_modules/snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
"license": "MIT",
"peer": true,
"dependencies": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/snakecase-keys": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-8.0.1.tgz",
"integrity": "sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw==",
"license": "MIT",
"peer": true,
"dependencies": {
"map-obj": "^4.1.0",
"snake-case": "^3.0.4",
"type-fest": "^4.15.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/sonner": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.1.tgz",
@@ -5821,6 +6326,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/std-env": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
"license": "MIT",
"peer": true
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -6014,6 +6526,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swr": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.3.tgz",
"integrity": "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==",
"license": "MIT",
"peer": true,
"dependencies": {
"dequal": "^2.0.3",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/tailwind-merge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
@@ -6153,6 +6679,19 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz",
"integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==",
"license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -6235,7 +6774,6 @@
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -6264,6 +6802,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT",
"peer": true
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@@ -6324,6 +6869,16 @@
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6451,6 +7006,16 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.24.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
"integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -13,6 +13,7 @@
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"autumn-js": "^0.0.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.483.0",

View File

@@ -1,104 +0,0 @@
"use server";
import { Autumn } from "@/sdk/autumn";
// ONLY 3 ENDPOINTS NEEDED TO GET STARTED
// Entitled: Check if your user should be allowed to use a feature
export const entitled = async ({
customerId,
featureId,
}: {
customerId: string;
featureId: string;
}) => {
const response = await fetch(`https://api.useautumn.com/v1/entitled`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AUTUMN_SECRET_KEY}`,
},
body: JSON.stringify({
customer_id: customerId,
feature_id: featureId,
}),
});
const data = await response.json();
return data.allowed;
};
//Events: send a usage event to Autumn to track a user's usage of a feature
export const sendEvent = async ({
customerId,
featureId,
}: {
customerId: string;
featureId: string;
}) => {
await fetch("https://api.useautumn.com/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AUTUMN_SECRET_KEY}`,
},
body: JSON.stringify({
customer_id: customerId,
feature_id: featureId,
}),
});
};
//Attach: get a checkout URL from Autumn so the user can upgrade
export const attachProduct = async ({
customerId,
productId,
}: {
customerId: string;
productId: string;
}) => {
const response = await fetch(`https://api.useautumn.com/v1/attach`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AUTUMN_SECRET_KEY}`,
},
body: JSON.stringify({
customer_id: customerId,
product_id: productId,
force_checkout: true,
}),
});
const data = await response.json();
if (response.status !== 200) {
throw new Error(data.message || "Failed to attach pro product");
}
return data;
};
// ADDITIONAL ENDPOINT TO FETCH CUSTOMER DETAILS
export const getOrCreateCustomer = async (customerId: string) => {
const response = await fetch(`https://api.useautumn.com/v1/customers`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AUTUMN_SECRET_KEY}`,
},
body: JSON.stringify({
id: customerId,
}),
});
const data = await response.json();
return data;
};
export const getCustomer = async (customerId: string) => {
const autumn = new Autumn();
const data = await autumn.customers.get(customerId);
const { entitlements, invoices } = data;
return data;
};

27
example/src/app/demo.tsx Normal file
View File

@@ -0,0 +1,27 @@
// <div className="text-2xl font-bold">{customer?.name}</div>
// <button
// onClick={async () => {
// await attach({
// productId: "pro-example",
// });
// }}
// >
// Upgrade to pro
// </button>
// import Application from "@/components/application";
// import CustomerDetailsExample from "@/components/billing";
// import Intro from "@/components/introduction";
// export default function Home() {
// return (
// <div className="min-h-screen w-full p-6 flex flex-col gap-8 max-w-7xl mx-auto">
// <Intro />
// <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
// <Application />
// <CustomerDetailsExample />
// </div>
// </div>
// );
// }

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Toaster } from "@/components/ui/sonner";
import { AutumnProvider } from "autumn-js/next";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -25,12 +26,20 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
<AutumnProvider
customerId="123"
customerData={{
name: "ayush",
email: "ayush@example.com",
}}
>
<Toaster position="top-right" />
{children}
</body>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Toaster position="top-right" />
{children}
</body>
</AutumnProvider>
</html>
);
}

View File

@@ -1,40 +1,21 @@
"use client";
// import { getOrCreateCustomer } from "./autumn-functions";
// import { useState } from "react";
// import { useEffect } from "react";
import Intro from "@/components/introduction";
// import { Autumn } from "useautumn";
// import CustomerDetailsExample from "@/components/billing";
// import Application from "@/components/application";
// Replace this with your internal user ID
import { useAutumn } from "autumn-js/next";
export default function Home() {
// const [customerData, setCustomerData] = useState<any>(null);
// const fetchCustomer = async () => {
// const customer = await getOrCreateCustomer(CUSTOMER_ID);
// setCustomerData(customer);
// };
// useEffect(() => {
// fetchCustomer();
// }, []);
// if (!customerData) {
// return <></>;
// }
const { customer, attach } = useAutumn();
return (
<div className="min-h-screen w-full p-6 flex flex-col gap-8 max-w-7xl mx-auto">
<Intro />
{/* <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Application customerId={CUSTOMER_ID} fetchCustomer={fetchCustomer} />
<CustomerDetailsExample customerData={customerData} />
</div> */}
<div className="h-screen w-full flex flex-col gap-4 items-center justify-center">
<div className="text-2xl font-bold">{customer?.name}</div>
<button
onClick={async () => {
await attach({
productId: "pro-example",
});
}}
>
Upgrade
</button>
</div>
);
}

View File

@@ -1,19 +1,12 @@
import { sendEvent } from "@/app/autumn-functions";
import { entitled } from "@/app/autumn-functions";
import { useAutumn } from "autumn-js/next";
import { MessageSquare } from "lucide-react";
import { toast } from "sonner";
export default function Application({
customerId,
fetchCustomer,
}: {
customerId: string;
fetchCustomer: () => void;
}) {
export default function Application() {
const { entitled, sendEvent, refetch } = useAutumn();
const sendMessageClicked = async (featureId: string) => {
const allowed = await entitled({
customerId,
const { allowed } = await entitled({
featureId,
});
@@ -23,10 +16,11 @@ export default function Application({
}
await sendEvent({
customerId,
featureId,
});
await refetch();
toast.success(`${featureId} used!`);
};
@@ -61,21 +55,10 @@ export default function Application({
<button
className="w-full bg-purple-600 hover:bg-purple-700 transition-colors"
onClick={async () => {
await sendMessageClicked("message-credits");
await fetchCustomer();
await sendMessageClicked("chat-messages");
}}
>
Use Standard Message
</button>
<button
className="w-full bg-purple-600 hover:bg-purple-700 transition-colors"
onClick={async () => {
await sendMessageClicked("premium-credits");
await fetchCustomer();
}}
>
Use Claude Message
Use Chat Message
</button>
</div>
</div>

View File

@@ -1,49 +1,40 @@
"use client";
import { attachProduct } from "@/app/autumn-functions";
import { CreditCard } from "lucide-react";
import { toast } from "sonner";
import { useAutumn } from "autumn-js/next";
export default function CustomerDetailsExample() {
const { customer, attach, openBillingPortal } = useAutumn();
const productId = "pro-example";
export default function CustomerDetailsExample({
customerData,
}: {
customerData: any;
}) {
const { customer, entitlements, products } = customerData;
const getEntitlement = (featureId: string) => {
return entitlements.find(
return customer?.features.find(
(entitlement: any) => entitlement.feature_id === featureId
);
};
const upgradeClicked = async () => {
try {
const res = await attachProduct({
customerId: customer.id,
productId: "pro",
await attach({
productId,
});
window.open(res.checkout_url, "_blank");
} catch (error: any) {
toast.error(`${error}`);
toast.error(`${error.message}`);
}
};
const buyExtraCreditsClicked = async () => {
const manageBillingClicked = async () => {
try {
const res = await attachProduct({
customerId: customer.id,
productId: "extra-credits",
});
window.open(res.checkout_url, "_blank");
await openBillingPortal();
} catch (error: any) {
toast.error(`${error}`);
toast.error(`${error.message}`);
}
};
const messageCredits = getEntitlement("message-credits");
const premiumCredits = getEntitlement("premium-credits");
const hasPro = products.length > 0 && products[0].id === "pro";
const messageCredits = getEntitlement("chat-messages");
const hasPro =
customer?.products?.length && customer?.products[0].id === productId;
return (
<div className="border rounded-lg bg-white overflow-hidden flex flex-col">
@@ -66,21 +57,16 @@ export default function CustomerDetailsExample({
<div className="flex items-center justify-between py-2 border-b">
<span className="text-sm font-medium">Customer ID</span>
<span className="text-sm font-mono bg-stone-50 px-2 py-1 rounded">
{customer.id}
{customer?.id}
</span>
</div>
<div className="flex items-center justify-between py-2 border-b">
<span className="text-sm font-medium">Standard Messages Remaining</span>
<span className="text-sm font-medium">Chat Messages Remaining</span>
<span className="text-sm font-mono bg-stone-50 px-2 py-1 rounded">
{messageCredits?.balance || 0}
</span>
</div>
<div className="flex items-center justify-between py-2 border-b">
<span className="text-sm font-medium">Premium Messages Remaining</span>
<span className="text-sm font-mono bg-stone-50 px-2 py-1 rounded">
{premiumCredits?.balance || 0}
{messageCredits?.unlimited
? "Unlimited"
: messageCredits?.balance || 0}
</span>
</div>
@@ -101,24 +87,16 @@ export default function CustomerDetailsExample({
</div>
</div>
<div className="flex gap-2 p-6 pt-0">
<div className="w-full pt-0">
{!hasPro && (
<button
className="w-full"
onClick={upgradeClicked}
>
Upgrade to Pro
<button className="w-full" onClick={upgradeClicked}>
<div className="w-full pt-0">Upgrade to Pro</div>
</button>
)}
</div>
<div className="w-full pt-0">
<button
className="w-full"
onClick={buyExtraCreditsClicked}
>
Buy Extra Premium Credits
<div className="w-full pt-0">
<button className="w-full" onClick={manageBillingClicked}>
Manage Billing
</button>
</div>
</div>
</div>
</div>
);

View File

@@ -1,361 +0,0 @@
import { CreateRewardProgram, ErrCode } from "@autumn/shared";
export default class AutumnError extends Error {
message: string;
code: string;
constructor({ message, code }: { message: string; code: string }) {
super(message);
this.message = message;
this.code = code;
}
toString(): string {
return `${this.message} (code: ${this.code})`;
}
}
export class Autumn {
private apiKey: string;
public headers: Record<string, string>;
public baseUrl: string;
constructor(apiKey?: string, baseUrl?: string) {
this.apiKey = apiKey || process.env.AUTUMN_SECRET_KEY || "";
this.headers = {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
};
this.baseUrl = "https://api.useautumn.com/v1";
// this.baseUrl = baseUrl || "http://localhost:8080/v1";
}
async get(path: string) {
const response = await fetch(`${this.baseUrl}${path}`, {
headers: this.headers,
});
return response.json();
}
async post(path: string, body: any) {
const response = await fetch(`${this.baseUrl}${path}`, {
method: "POST",
headers: this.headers,
body: JSON.stringify(body),
});
if (response.status != 200) {
let error: any;
try {
error = await response.json();
} catch (error) {
throw new AutumnError({
message: "Failed to parse Autumn API error response",
code: ErrCode.InternalError,
});
}
throw new AutumnError({
message: error.message,
code: error.code,
});
}
return response.json();
}
async delete(
path: string,
{
deleteInStripe = false,
}: {
deleteInStripe?: boolean;
} = {}
) {
const response = await fetch(
`${this.baseUrl}${path}?${deleteInStripe ? "delete_in_stripe=true" : ""}`,
{
method: "DELETE",
headers: this.headers,
}
);
if (response.status != 200) {
let error: any;
try {
error = await response.json();
} catch (error) {
throw new AutumnError({
message: "Failed to parse Autumn API error response",
code: ErrCode.InternalError,
});
}
throw new AutumnError({
message: error.message,
code: error.code,
});
}
return response.json();
}
async createCustomer({
id,
email,
name,
fingerprint,
}: {
id: string;
email: string;
name: string;
fingerprint?: string;
}) {
const data = await this.post("/customers", {
id,
email,
name,
fingerprint,
});
return data;
}
async attach({
customerId,
productId,
options,
}: {
customerId: string;
productId: string;
options?: any;
}) {
const data = await this.post(`/attach`, {
customer_id: customerId,
product_id: productId,
options,
});
return data;
}
async sendEvent({
customerId,
eventName,
properties,
customer_data,
idempotency_key,
}: {
customerId: string;
eventName: string;
properties?: any;
customer_data?: any;
idempotency_key?: string;
}) {
const data = await this.post(`/events`, {
customer_id: customerId,
event_name: eventName,
properties,
customer_data,
idempotency_key,
});
return data;
}
async entitled({
customerId,
featureId,
quantity,
customer_data,
}: {
customerId: string;
featureId: string;
quantity?: number;
customer_data?: any;
}) {
const data = await this.post(`/entitled`, {
customer_id: customerId,
feature_id: featureId,
quantity,
customer_data,
});
return data;
}
customers = {
get: async (customerId: string) => {
const data = await this.get(`/customers/${customerId}`);
return data;
},
create: async (customer: { id: string; email: string; name: string }) => {
const data = await this.post(`/customers`, customer);
return data;
},
delete: async (
customerId: string,
{
deleteInStripe = false,
}: {
deleteInStripe?: boolean;
} = {}
) => {
const data = await this.delete(`/customers/${customerId}`, {
deleteInStripe,
});
return data;
},
};
entities = {
create: async (
customerId: string,
entity:
| {
id: string;
name: string;
featureId: string;
}
| {
id: string;
name: string;
featureId: string;
}[]
) => {
let entities = Array.isArray(entity) ? entity : [entity];
const data = await this.post(
`/customers/${customerId}/entities`,
entities.map((e: any) => {
return {
id: e.id,
name: e.name,
feature_id: e.featureId,
};
})
);
return data;
},
list: async (customerId: string) => {
const data = await this.get(`/customers/${customerId}/entities`);
return data;
},
delete: async (customerId: string, entityId: string) => {
const data = await this.delete(
`/customers/${customerId}/entities/${entityId}`
);
return data;
},
};
products = {
update: async (productId: string, product: any) => {
if (product.items && typeof product.items === "object") {
product.items = Object.values(product.items);
}
const data = await this.post(`/products/${productId}`, product);
return data;
},
get: async (
productId: string,
{ v1Schema = false }: { v1Schema?: boolean } = {}
) => {
const data = await this.get(
`/products/${productId}?${v1Schema ? "schemaVersion=1" : ""}`
);
return data;
},
create: async (product: any) => {
const data = await this.post(`/products`, product);
return data;
},
delete: async (productId: string) => {
const data = await this.delete(`/products/${productId}`);
return data;
},
};
rewards = {
create: async (reward: any) => {
const data = await this.post(`/rewards`, reward);
return data;
},
};
rewardPrograms = {
create: async (rewardProgram: CreateRewardProgram) => {
const data = await this.post(`/reward_programs`, rewardProgram);
return data;
},
};
referrals = {
createCode: async ({
customerId,
referralId,
}: {
customerId: string;
referralId: string;
}) => {
const data = await this.post(`/referrals/code`, {
customer_id: customerId,
program_id: referralId,
});
return data;
},
redeem: async ({
customerId,
code,
}: {
customerId: string;
code: string;
}) => {
const data = await this.post(`/referrals/redeem`, {
customer_id: customerId,
code,
});
return data;
},
};
redemptions = {
get: async ({ redemptionId }: { redemptionId: string }) => {
const data = await this.get(`/redemptions/${redemptionId}`);
return data;
},
};
events = {
send: async ({
customerId,
featureId,
value,
properties,
}: {
customerId: string;
featureId: string;
value: number;
properties?: any;
}) => {
const data = await this.post(`/events`, {
customer_id: customerId,
feature_id: featureId,
value,
properties,
});
return data;
},
};
initStripe = async () => {
await this.post(`/products/all/init_stripe`, {});
};
}

View File

@@ -258,16 +258,32 @@ export const handleClerkWebhook = async (req: any, res: any) => {
});
};
const handleOrgCreated = async (sb: SupabaseClient, eventData: any) => {
export const handleOrgCreated = async (
sb: SupabaseClient,
eventData: {
id: string;
slug: string;
created_at: number;
}
) => {
console.log(
`Handling organization.created: ${eventData.slug} (${eventData.id})`
);
try {
// 1. Create svix webhoooks
const { sandboxApp, liveApp } = await initOrgSvixApps({
slug: eventData.slug,
id: eventData.id,
});
// // Check if org already exists
// let org = await OrgService.getFullOrg({
// sb,
// orgId: eventData.id,
// errorIfNotFound: false,
// });
// if (org) {
// console.log(
// `handleOrgCreated: Org already exists: ${eventData.slug} (${eventData.id})`
// );
// return;
// }
// 2. Insert org
await OrgService.insert({
@@ -282,16 +298,39 @@ const handleOrgCreated = async (sb: SupabaseClient, eventData: any) => {
live_pkey: generatePublishableKey(AppEnv.Live),
created_at: eventData.created_at,
svix_config: {
sandbox_app_id: sandboxApp.id,
live_app_id: liveApp.id,
sandbox_app_id: "",
live_app_id: "",
},
config: {} as any,
},
});
console.log(`Inserted org ${eventData.id}`);
} catch (error) {
// 1. Create svix webhoooks
const { sandboxApp, liveApp } = await initOrgSvixApps({
slug: eventData.slug,
id: eventData.id,
});
await OrgService.update({
sb,
orgId: eventData.id,
updates: {
svix_config: { sandbox_app_id: sandboxApp.id, live_app_id: liveApp.id },
},
});
console.log(`Created svix webhooks for org ${eventData.id}`);
} catch (error: any) {
if (error?.data && error.data.code == "23505") {
console.error(
`Org ${eventData.id} already exists in Supabase -- skipping creationg`
);
return;
}
console.error("Failed to insert org", error);
return;
}
const batch = [];

View File

@@ -53,6 +53,7 @@ apiRouter.use((req: any, res: any, next: any) => {
let skipUrls = ["/v1/customers/all/search"];
res.on("finish", () => {
try {
req.logtail.flush();
if (skipUrls.includes(req.originalUrl)) {
return;
}

View File

@@ -8,6 +8,12 @@ import { productRouter } from "./products/internalProductRouter.js";
import { devRouter } from "./dev/devRouter.js";
import { cusRouter } from "./customers/internalCusRouter.js";
import { testRouter } from "./test/testRouter.js";
import { createClerkCli } from "../external/clerkUtils.js";
import { ErrCode } from "@autumn/shared";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { STATUS_CODES } from "http";
import { StatusCodes } from "http-status-codes";
import { handleOrgCreated } from "@/external/webhooks/clerkWebhooks.js";
const mainRouter = Router();
@@ -15,6 +21,51 @@ mainRouter.get("", (req: any, res) => {
res.status(200).json({ message: "Hello World" });
});
mainRouter.post("/organization", withAuth, async (req: any, res) => {
try {
let { orgId } = req.body;
let userId = req.userId;
let clerk = createClerkCli();
let org = await clerk.organizations.getOrganization({
organizationId: orgId,
});
if (!org) {
throw new RecaseError({
message: "Organization not found",
code: ErrCode.OrgNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
const memberships = await clerk.users.getOrganizationMembershipList({
userId: userId,
});
let userIsAdmin = memberships.data.some((m) => m.organization.id === orgId);
if (!userIsAdmin) {
throw new RecaseError({
message: "User is not an admin of this organization",
code: ErrCode.OrgNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
handleOrgCreated(req.sb, {
id: orgId,
slug: org.slug || "",
created_at: Date.now(),
});
res.status(200).json({ message: "Success" });
} catch (error: any) {
handleRequestError({ error, req, res, action: "create org" });
}
});
mainRouter.use("/users", withAuth, userRouter);
mainRouter.use("/organization", withOrgAuth, orgRouter);
mainRouter.use("/features", withOrgAuth, featureRouter);

View File

@@ -12,6 +12,7 @@ import { OrgService } from "./OrgService.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { AppEnv } from "@autumn/shared";
import { nullish } from "@/utils/genUtils.js";
import { handleOrgCreated } from "@/external/webhooks/clerkWebhooks.js";
export const orgRouter = express.Router();

View File

@@ -1,6 +1,8 @@
import Step from "@/components/general/OnboardingStep";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useEnv } from "@/utils/envUtils";
import { useOrganization, useOrganizationList } from "@clerk/clerk-react";
import { Building } from "lucide-react";
import { useState } from "react";
@@ -15,6 +17,8 @@ export const CreateOrgStep = ({
pollForOrg: () => Promise<void>;
}) => {
const { organization: org } = useOrganization();
let env = useEnv();
let axios = useAxiosInstance({ env });
const { createOrganization, setActive } = useOrganizationList();
const [isExploding, setIsExploding] = useState(false);
@@ -37,6 +41,11 @@ export const CreateOrgStep = ({
name: fields.name,
});
// Create org in Autumn
await axios.post("/organization", {
orgId: org.id,
});
await setActive({ organization: org.id });
// await pollForOrg();
toast.success(`Created your organization: ${org.name}`);