mirror of
https://github.com/openshiporg/openship.git
synced 2026-06-19 07:35:55 +00:00
refactor: Improve CreateChannelFromURL and CreateShopFromURL components for better readability and maintainability
- Enhanced conditional checks and removed unnecessary console logs. - Updated capitalization for "Openfront" in various components. - Improved handling of URL parameters after successful channel/shop creation. - Refactored badge components to use a consistent style across PlatformTabs, StatusTabs, and MatchesTabs. - Added delete functionality with confirmation dialog in OrderDetailsComponent. - Enhanced logging in createShop action for better debugging.
This commit is contained in:
+534
-217
@@ -150,7 +150,7 @@ async function getProductFunction({
|
||||
console.log("OpenFront Channel getProductFunction called with:", { platform: platform.domain, productId, variantId });
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
const gqlQuery = import_graphql_request.gql`
|
||||
query GetChannelProduct($productId: ID!, $variantId: ID) {
|
||||
query GetChannelProduct($productId: ID!, $variantWhere: ProductVariantWhereInput) {
|
||||
product(where: { id: $productId }) {
|
||||
id
|
||||
title
|
||||
@@ -161,7 +161,7 @@ async function getProductFunction({
|
||||
}
|
||||
imagePath
|
||||
}
|
||||
productVariants(where: $variantId ? { id: { equals: $variantId } } : {}) {
|
||||
productVariants(where: $variantWhere) {
|
||||
id
|
||||
title
|
||||
sku
|
||||
@@ -180,7 +180,7 @@ async function getProductFunction({
|
||||
`;
|
||||
const { product } = await openFrontClient.request(gqlQuery, {
|
||||
productId,
|
||||
variantId
|
||||
variantWhere: variantId ? { id: { equals: variantId } } : {}
|
||||
});
|
||||
if (!product || product.status !== "published") {
|
||||
throw new Error("Product not available for fulfillment from OpenFront");
|
||||
@@ -211,91 +211,161 @@ async function createPurchaseFunction({
|
||||
}) {
|
||||
console.log(`\u{1F6D2} OpenFront Channel: Creating purchase with ${cartItems.length} items`);
|
||||
console.log(`\u{1F69A} OpenFront Channel: Ship to: ${shipping?.firstName} ${shipping?.lastName}`);
|
||||
console.log(`\u{1F4E6} OpenFront Channel: Full shipping data:`, JSON.stringify(shipping, null, 2));
|
||||
console.log(`\u{1F4E6} OpenFront Channel: Platform data:`, JSON.stringify(platform, null, 2));
|
||||
console.log(`\u{1F4E6} OpenFront Channel: Cart items:`, JSON.stringify(cartItems, null, 2));
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
const purchaseId = `PO-OF-${Date.now()}`;
|
||||
const orderNumber = `#${purchaseId}`;
|
||||
const totalPrice = cartItems.reduce((sum, item) => {
|
||||
return sum + parseFloat(item.price) * item.quantity;
|
||||
}, 0);
|
||||
const createOrderMutation = import_graphql_request.gql`
|
||||
mutation CreateFulfillmentOrder($data: OrderCreateInput!) {
|
||||
createOrder(data: $data) {
|
||||
id
|
||||
orderNumber
|
||||
total
|
||||
status
|
||||
orderLineItems {
|
||||
try {
|
||||
const currencyCode = (shipping?.currency || "USD").toLowerCase();
|
||||
const getRegionQuery = import_graphql_request.gql`
|
||||
query GetRegion($currencyCode: String!) {
|
||||
regions(where: { currency: { code: { equals: $currencyCode } } }) {
|
||||
id
|
||||
title
|
||||
quantity
|
||||
unitPrice
|
||||
productVariant {
|
||||
taxRate
|
||||
currency {
|
||||
id
|
||||
title
|
||||
sku
|
||||
product {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const { regions } = await openFrontClient.request(getRegionQuery, {
|
||||
currencyCode
|
||||
});
|
||||
const region = regions[0];
|
||||
if (!region) {
|
||||
throw new Error(`No region found for currency: ${currencyCode}`);
|
||||
}
|
||||
console.log("\u{1F6D2} [OpenFront Channel] Creating cart with region:", region.id);
|
||||
const { createCart: cart } = await openFrontClient.request(import_graphql_request.gql`
|
||||
mutation CreateCart($data: CartCreateInput!) {
|
||||
createCart(data: $data) {
|
||||
id
|
||||
region {
|
||||
id
|
||||
currency {
|
||||
id
|
||||
title
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
try {
|
||||
const orderData = {
|
||||
orderNumber,
|
||||
customerEmail: shipping?.email || "fulfillment@openship.org",
|
||||
firstName: shipping?.firstName || "Fulfillment",
|
||||
lastName: shipping?.lastName || "Order",
|
||||
address1: shipping?.streetAddress1 || "",
|
||||
address2: shipping?.streetAddress2 || "",
|
||||
city: shipping?.city || "",
|
||||
state: shipping?.state || "",
|
||||
postalCode: shipping?.zip || "",
|
||||
countryCode: shipping?.country || "US",
|
||||
phone: shipping?.phone || "",
|
||||
total: Math.round(totalPrice * 100),
|
||||
// Convert to cents
|
||||
subtotal: Math.round(totalPrice * 100),
|
||||
status: "pending_fulfillment",
|
||||
orderLineItems: {
|
||||
create: cartItems.map((item) => ({
|
||||
title: item.name || `Product ${item.variantId}`,
|
||||
quantity: item.quantity,
|
||||
unitPrice: Math.round(parseFloat(item.price) * 100),
|
||||
// Convert to cents
|
||||
productVariant: { connect: { id: item.variantId } }
|
||||
}))
|
||||
`, {
|
||||
data: {
|
||||
region: { connect: { id: region.id } },
|
||||
email: shipping?.email || `order-${Date.now()}@openship.generated`
|
||||
}
|
||||
};
|
||||
const result = await openFrontClient.request(createOrderMutation, {
|
||||
data: orderData
|
||||
});
|
||||
const order = result.createOrder;
|
||||
console.log(`\u{1F4E7} OpenFront Channel: Fulfillment Order Created: ${orderNumber}`);
|
||||
console.log(`\u{1F4B0} OpenFront Channel: Total: $${totalPrice.toFixed(2)}`);
|
||||
const processedLineItems = order.orderLineItems.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
console.log("\u2705 [OpenFront Channel] Cart created with ID:", cart.id);
|
||||
const lineItemsToCreate = [];
|
||||
for (const item of cartItems) {
|
||||
lineItemsToCreate.push({
|
||||
productVariant: { connect: { id: item.variantId } },
|
||||
quantity: item.quantity
|
||||
});
|
||||
}
|
||||
console.log("\u{1F4E6} [OpenFront Channel] Adding", lineItemsToCreate.length, "line items to cart");
|
||||
await openFrontClient.request(import_graphql_request.gql`
|
||||
mutation AddLineItemsToCart($cartId: ID!, $data: CartUpdateInput!) {
|
||||
updateActiveCart(cartId: $cartId, data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`, {
|
||||
cartId: cart.id,
|
||||
data: {
|
||||
lineItems: {
|
||||
create: lineItemsToCreate
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log("\u2705 [OpenFront Channel] Line items added to cart");
|
||||
console.log("\u{1F4CD} [OpenFront Channel] Creating shipping address");
|
||||
const { createAddress: shippingAddr } = await openFrontClient.request(import_graphql_request.gql`
|
||||
mutation CreateAddress($data: AddressCreateInput!) {
|
||||
createAddress(data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`, {
|
||||
data: {
|
||||
firstName: shipping?.firstName || "Guest",
|
||||
lastName: shipping?.lastName || "Customer",
|
||||
address1: shipping?.address1 || "123 Default St",
|
||||
city: shipping?.city || "Default City",
|
||||
province: shipping?.state || "NY",
|
||||
postalCode: shipping?.zip || "10001",
|
||||
phone: shipping?.phone || "",
|
||||
country: {
|
||||
connect: {
|
||||
iso2: (shipping?.country || "US").toLowerCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log("\u2705 [OpenFront Channel] Shipping address created:", shippingAddr.id);
|
||||
await openFrontClient.request(import_graphql_request.gql`
|
||||
mutation UpdateCartAddresses($cartId: ID!, $data: CartUpdateInput!) {
|
||||
updateActiveCart(cartId: $cartId, data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`, {
|
||||
cartId: cart.id,
|
||||
data: {
|
||||
shippingAddress: { connect: { id: shippingAddr.id } },
|
||||
billingAddress: { connect: { id: shippingAddr.id } }
|
||||
}
|
||||
});
|
||||
console.log("\u2705 [OpenFront Channel] Cart updated with addresses");
|
||||
console.log("\u{1F3AF} [OpenFront Channel] Completing cart to create order");
|
||||
console.log("\u{1F3AF} [OpenFront Channel] Calling completeActiveCart for cart:", cart.id);
|
||||
const completeResult = await openFrontClient.request(import_graphql_request.gql`
|
||||
mutation CompleteActiveCart($cartId: ID!) {
|
||||
completeActiveCart(cartId: $cartId)
|
||||
}
|
||||
`, {
|
||||
cartId: cart.id
|
||||
});
|
||||
console.log("\u{1F50D} [OpenFront Channel] completeActiveCart result:", JSON.stringify(completeResult, null, 2));
|
||||
const order = completeResult.completeActiveCart;
|
||||
if (!order?.id) {
|
||||
throw new Error("Failed to complete cart - no order created");
|
||||
}
|
||||
console.log("\u{1F50D} [OpenFront Channel] COMPLETE ORDER RESULT:", JSON.stringify(order, null, 2));
|
||||
console.log("\u{1F389} [OpenFront Channel] Order created successfully:", order.id);
|
||||
const processedLineItems = cartItems.map((item) => ({
|
||||
id: item.variantId,
|
||||
title: item.name || `Product ${item.variantId}`,
|
||||
quantity: item.quantity,
|
||||
variantId: item.productVariant.id,
|
||||
productId: item.productVariant.product.id
|
||||
variantId: item.variantId
|
||||
}));
|
||||
return {
|
||||
purchaseId: order.id,
|
||||
orderNumber: order.orderNumber,
|
||||
totalPrice: (order.total / 100).toFixed(2),
|
||||
orderNumber: `#${order.displayId}`,
|
||||
totalPrice: order.total,
|
||||
invoiceUrl: `https://${platform.domain}/admin/orders/${order.id}`,
|
||||
lineItems: processedLineItems,
|
||||
status: "processing"
|
||||
status: "pending"
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("OpenFront Channel: Purchase creation failed:", error);
|
||||
console.error("\u{1F6A8} OpenFront Channel: Purchase creation failed:", error);
|
||||
if (error.response?.errors) {
|
||||
console.error("\u{1F6A8} GraphQL Errors:");
|
||||
error.response.errors.forEach((err, i) => {
|
||||
console.error(` ${i + 1}. ${err.message}`);
|
||||
if (err.path) {
|
||||
console.error(` Path: ${err.path.join(" -> ")}`);
|
||||
}
|
||||
if (err.extensions) {
|
||||
console.error(` Extensions:`, err.extensions);
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
purchaseId,
|
||||
orderNumber,
|
||||
totalPrice: totalPrice.toFixed(2),
|
||||
purchaseId: null,
|
||||
orderNumber: null,
|
||||
totalPrice: "0.00",
|
||||
invoiceUrl: null,
|
||||
lineItems: cartItems.map((item) => ({
|
||||
id: `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
@@ -314,51 +384,123 @@ async function createWebhookFunction({
|
||||
endpoint,
|
||||
events
|
||||
}) {
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
const createWebhookMutation = import_graphql_request.gql`
|
||||
mutation CreateChannelWebhookEndpoint($data: WebhookEndpointCreateInput!) {
|
||||
createWebhookEndpoint(data: $data) {
|
||||
id
|
||||
url
|
||||
events
|
||||
isActive
|
||||
secret
|
||||
console.log("\u{1FA9D} [OpenFront Channel] createWebhookFunction called");
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Platform domain:", platform.domain);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Endpoint URL:", endpoint);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Events received:", events);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Events type:", typeof events, "Array?", Array.isArray(events));
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Platform data:", JSON.stringify(platform, null, 2));
|
||||
const eventMap = {
|
||||
ORDER_CREATED: "order.created",
|
||||
ORDER_CANCELLED: "order.cancelled",
|
||||
TRACKING_CREATED: "fulfillment.created"
|
||||
};
|
||||
const openFrontEvents = events.map((event) => {
|
||||
const mapped = eventMap[event] || event;
|
||||
console.log(`\u{1FA9D} [OpenFront Channel] Mapping: ${event} -> ${mapped}`);
|
||||
return mapped;
|
||||
});
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Final mapped events array:", openFrontEvents);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Joined events string:", openFrontEvents.join(", "));
|
||||
try {
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Creating OpenFront GraphQL client...");
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
console.log("\u2705 [OpenFront Channel] GraphQL client created successfully");
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Fetching current user...");
|
||||
const getUserQuery = import_graphql_request.gql`
|
||||
query GetCurrentUser {
|
||||
authenticatedItem {
|
||||
... on User {
|
||||
id
|
||||
email
|
||||
orderWebhookUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Executing getUserQuery...");
|
||||
const { authenticatedItem: user } = await openFrontClient.request(getUserQuery);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] getUserQuery result:", JSON.stringify(user, null, 2));
|
||||
if (!user) {
|
||||
console.error("\u{1F6A8} [OpenFront Channel] User not authenticated - authenticatedItem is null/undefined");
|
||||
throw new Error("User not authenticated");
|
||||
}
|
||||
console.log("\u2705 [OpenFront Channel] User authenticated successfully");
|
||||
console.log("\u{1FA9D} [OpenFront Channel] User ID:", user.id);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] User email:", user.email);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Current webhook URL:", user.orderWebhookUrl);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Updating user webhook URL...");
|
||||
const updateUserMutation = import_graphql_request.gql`
|
||||
mutation UpdateActiveUserWebhookUrl($data: UserUpdateProfileInput!) {
|
||||
updateActiveUser(data: $data) {
|
||||
id
|
||||
orderWebhookUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Update mutation variables:");
|
||||
console.log("\u{1FA9D} [OpenFront Channel] - User ID:", user.id);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] - New webhook URL:", endpoint);
|
||||
const result = await openFrontClient.request(updateUserMutation, {
|
||||
data: { orderWebhookUrl: endpoint }
|
||||
});
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Update mutation result:", JSON.stringify(result, null, 2));
|
||||
const updatedUser = result.updateActiveUser;
|
||||
console.log("\u2705 [OpenFront Channel] User webhook URL updated successfully");
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Updated user ID:", updatedUser.id);
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Updated webhook URL:", updatedUser.orderWebhookUrl);
|
||||
const webhookResponse = {
|
||||
webhooks: [{
|
||||
id: `user-${updatedUser.id}`,
|
||||
callbackUrl: updatedUser.orderWebhookUrl,
|
||||
topic: openFrontEvents.join(", "),
|
||||
// Join array into comma-separated string
|
||||
format: "JSON",
|
||||
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
||||
}],
|
||||
webhookId: `user-${updatedUser.id}`
|
||||
};
|
||||
console.log("\u{1FA9D} [OpenFront Channel] Final webhook response:", JSON.stringify(webhookResponse, null, 2));
|
||||
return webhookResponse;
|
||||
} catch (error) {
|
||||
console.error("\u{1F6A8} [OpenFront Channel] createWebhookFunction failed:", error);
|
||||
console.error("\u{1F6A8} [OpenFront Channel] Error message:", error.message);
|
||||
console.error("\u{1F6A8} [OpenFront Channel] Error stack:", error.stack);
|
||||
if (error.response) {
|
||||
console.error("\u{1F6A8} [OpenFront Channel] GraphQL response:", JSON.stringify(error.response, null, 2));
|
||||
if (error.response.errors) {
|
||||
console.error("\u{1F6A8} [OpenFront Channel] GraphQL errors:");
|
||||
error.response.errors.forEach((err, i) => {
|
||||
console.error(`\u{1F6A8} [OpenFront Channel] ${i + 1}. ${err.message}`);
|
||||
if (err.path) {
|
||||
console.error(`\u{1F6A8} [OpenFront Channel] Path: ${err.path.join(" -> ")}`);
|
||||
}
|
||||
if (err.extensions) {
|
||||
console.error(`\u{1F6A8} [OpenFront Channel] Extensions:`, err.extensions);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
`;
|
||||
const eventMap = {
|
||||
PURCHASE_CREATED: "order.created",
|
||||
PURCHASE_SHIPPED: "fulfillment.shipped",
|
||||
INVENTORY_UPDATED: "inventory.updated"
|
||||
};
|
||||
const openFrontEvents = events.map((event) => eventMap[event] || event);
|
||||
const result = await openFrontClient.request(createWebhookMutation, {
|
||||
data: {
|
||||
url: endpoint,
|
||||
events: openFrontEvents,
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
const webhook = result.createWebhookEndpoint;
|
||||
return {
|
||||
webhooks: [webhook],
|
||||
webhookId: webhook.id
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async function deleteWebhookFunction({
|
||||
platform,
|
||||
webhookId
|
||||
}) {
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
const deleteWebhookMutation = import_graphql_request.gql`
|
||||
mutation DeleteChannelWebhookEndpoint($where: WebhookEndpointWhereUniqueInput!) {
|
||||
deleteWebhookEndpoint(where: $where) {
|
||||
const userId = webhookId.replace("user-", "");
|
||||
const updateUserMutation = import_graphql_request.gql`
|
||||
mutation ClearUserWebhookUrl($where: UserWhereUniqueInput!, $data: UserUpdateInput!) {
|
||||
updateUser(where: $where, data: $data) {
|
||||
id
|
||||
orderWebhookUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = await openFrontClient.request(deleteWebhookMutation, {
|
||||
where: { id: webhookId }
|
||||
const result = await openFrontClient.request(updateUserMutation, {
|
||||
where: { id: userId },
|
||||
data: { orderWebhookUrl: null }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -366,30 +508,34 @@ async function getWebhooksFunction({
|
||||
platform
|
||||
}) {
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
const query = import_graphql_request.gql`
|
||||
query GetChannelWebhookEndpoints {
|
||||
webhookEndpoints(where: { isActive: { equals: true } }) {
|
||||
id
|
||||
url
|
||||
events
|
||||
isActive
|
||||
createdAt
|
||||
const eventMap = {
|
||||
"order.created": "PURCHASE_CREATED",
|
||||
"fulfillment.created": "PURCHASE_SHIPPED"
|
||||
};
|
||||
const getUserQuery = import_graphql_request.gql`
|
||||
query GetCurrentUser {
|
||||
authenticatedItem {
|
||||
... on User {
|
||||
id
|
||||
email
|
||||
orderWebhookUrl
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const { webhookEndpoints } = await openFrontClient.request(query);
|
||||
const eventMap = {
|
||||
"order.created": "PURCHASE_CREATED",
|
||||
"fulfillment.shipped": "PURCHASE_SHIPPED",
|
||||
"inventory.updated": "INVENTORY_UPDATED"
|
||||
};
|
||||
const webhooks = webhookEndpoints.map((webhook) => ({
|
||||
id: webhook.id,
|
||||
callbackUrl: webhook.url,
|
||||
topic: webhook.events.map((event) => eventMap[event] || event),
|
||||
const { authenticatedItem: user } = await openFrontClient.request(getUserQuery);
|
||||
if (!user || !user.orderWebhookUrl) {
|
||||
return { webhooks: [] };
|
||||
}
|
||||
const webhooks = [{
|
||||
id: `user-${user.id}`,
|
||||
callbackUrl: user.orderWebhookUrl,
|
||||
topic: ["PURCHASE_CREATED", "PURCHASE_SHIPPED"],
|
||||
// These are the channel events we support
|
||||
format: "JSON",
|
||||
createdAt: webhook.createdAt
|
||||
}));
|
||||
createdAt: user.createdAt
|
||||
}];
|
||||
return { webhooks };
|
||||
}
|
||||
async function addTrackingFunction({
|
||||
@@ -501,41 +647,119 @@ async function oAuthCallbackFunction({
|
||||
console.error("OpenFront OAuth error:", errorText);
|
||||
throw new Error(`Failed to exchange OAuth code for access token: ${response.statusText}`);
|
||||
}
|
||||
const { access_token } = await response.json();
|
||||
return access_token;
|
||||
const { access_token, refresh_token, expires_in } = await response.json();
|
||||
return {
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token,
|
||||
expiresIn: expires_in,
|
||||
tokenExpiresAt: new Date(Date.now() + expires_in * 1e3).toISOString()
|
||||
};
|
||||
}
|
||||
function scopes() {
|
||||
return REQUIRED_SCOPES;
|
||||
}
|
||||
var import_graphql_request, getFreshAccessToken, createOpenFrontClient, getProductImageUrl, REQUIRED_SCOPES;
|
||||
var import_graphql_request, import_context, getFreshAccessToken, createOpenFrontClient, getProductImageUrl, REQUIRED_SCOPES;
|
||||
var init_openfront = __esm({
|
||||
"features/integrations/channel/openfront.ts"() {
|
||||
"use strict";
|
||||
import_graphql_request = require("graphql-request");
|
||||
import_context = require("@/features/keystone/context");
|
||||
getFreshAccessToken = async (platform) => {
|
||||
if (platform.tokenExpiresAt && platform.refreshToken) {
|
||||
const expiresAt = typeof platform.tokenExpiresAt === "string" ? new Date(platform.tokenExpiresAt) : platform.tokenExpiresAt;
|
||||
if (expiresAt > /* @__PURE__ */ new Date()) {
|
||||
console.log("\u{1F504} [OpenFront Channel] getFreshAccessToken called");
|
||||
console.log("\u{1F504} [OpenFront Channel] Platform domain:", platform.domain);
|
||||
console.log("\u{1F504} [OpenFront Channel] Actual access token:", platform.accessToken);
|
||||
const channels = await import_context.keystoneContext.sudo().query.Channel.findMany({
|
||||
where: {
|
||||
domain: { equals: platform.domain },
|
||||
accessToken: { equals: platform.accessToken }
|
||||
},
|
||||
query: "id refreshToken tokenExpiresAt platform { appKey appSecret }"
|
||||
});
|
||||
if (!channels || channels.length === 0) {
|
||||
console.log("\u26A0\uFE0F [OpenFront Channel] No matching channel found in database");
|
||||
return platform.accessToken;
|
||||
}
|
||||
const channel = channels[0];
|
||||
console.log("\u{1F504} [OpenFront Channel] Found channel:", channel.id);
|
||||
console.log("\u{1F504} [OpenFront Channel] Has refresh token:", !!channel.refreshToken);
|
||||
console.log("\u{1F504} [OpenFront Channel] Token expires at:", channel.tokenExpiresAt);
|
||||
console.log("\u{1F504} [OpenFront Channel] Has appKey:", !!channel.platform?.appKey);
|
||||
console.log("\u{1F504} [OpenFront Channel] Has appSecret:", !!channel.platform?.appSecret);
|
||||
console.log("\u{1F504} [OpenFront Channel] Actual refresh token:", channel.refreshToken);
|
||||
if (channel.refreshToken) {
|
||||
console.log("\u{1F504} [OpenFront Channel] Refresh token found, checking if refresh needed");
|
||||
let shouldRefresh = false;
|
||||
if (channel.tokenExpiresAt) {
|
||||
const expiresAt = typeof channel.tokenExpiresAt === "string" ? new Date(channel.tokenExpiresAt) : channel.tokenExpiresAt;
|
||||
const now = /* @__PURE__ */ new Date();
|
||||
shouldRefresh = expiresAt <= now;
|
||||
console.log("\u{1F504} [OpenFront Channel] Token expiry check:");
|
||||
console.log("\u{1F504} [OpenFront Channel] - Expires at:", expiresAt.toISOString());
|
||||
console.log("\u{1F504} [OpenFront Channel] - Current time:", now.toISOString());
|
||||
console.log("\u{1F504} [OpenFront Channel] - Should refresh:", shouldRefresh);
|
||||
} else {
|
||||
shouldRefresh = true;
|
||||
console.log("\u{1F504} [OpenFront Channel] No expiry info found, assuming refresh needed");
|
||||
}
|
||||
if (shouldRefresh) {
|
||||
console.log("\u{1F504} [OpenFront Channel] Starting token refresh process...");
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
console.log("\u{1F504} [OpenFront Channel] Token URL:", tokenUrl);
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: channel.refreshToken,
|
||||
client_id: channel.platform?.appKey || "",
|
||||
client_secret: channel.platform?.appSecret || ""
|
||||
});
|
||||
console.log("\u{1F504} [OpenFront Channel] Refresh request params:");
|
||||
console.log("\u{1F504} [OpenFront Channel] - grant_type: refresh_token");
|
||||
console.log("\u{1F504} [OpenFront Channel] - client_id:", channel.platform?.appKey || "NOT_SET");
|
||||
console.log("\u{1F504} [OpenFront Channel] - client_secret:", channel.platform?.appSecret ? "SET" : "NOT_SET");
|
||||
console.log("\u{1F504} [OpenFront Channel] - refresh_token:", channel.refreshToken ? "SET" : "NOT_SET");
|
||||
console.log("\u{1F504} [OpenFront Channel] Making refresh token request...");
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData
|
||||
});
|
||||
console.log("\u{1F504} [OpenFront Channel] Refresh response status:", response.status);
|
||||
console.log("\u{1F504} [OpenFront Channel] Refresh response ok:", response.ok);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("\u{1F6A8} [OpenFront Channel] Token refresh failed:", errorText);
|
||||
console.error("\u{1F6A8} [OpenFront Channel] Response status:", response.status);
|
||||
console.error("\u{1F6A8} [OpenFront Channel] Response statusText:", response.statusText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
const tokenData = await response.json();
|
||||
console.log("\u{1F504} [OpenFront Channel] Token refresh response received");
|
||||
console.log("\u{1F504} [OpenFront Channel] - Has access_token:", !!tokenData.access_token);
|
||||
console.log("\u{1F504} [OpenFront Channel] - Has refresh_token:", !!tokenData.refresh_token);
|
||||
console.log("\u{1F504} [OpenFront Channel] - Expires in:", tokenData.expires_in, "seconds");
|
||||
const { access_token, refresh_token, expires_in } = tokenData;
|
||||
console.log("\u{1F504} [OpenFront Channel] Updating tokens in database...");
|
||||
try {
|
||||
console.log("\u{1F504} [OpenFront Channel] Updating channel:", channel.id);
|
||||
await import_context.keystoneContext.sudo().query.Channel.updateOne({
|
||||
where: { id: channel.id },
|
||||
data: {
|
||||
accessToken: access_token,
|
||||
...refresh_token && { refreshToken: refresh_token },
|
||||
...expires_in && { tokenExpiresAt: new Date(Date.now() + expires_in * 1e3) }
|
||||
}
|
||||
});
|
||||
console.log("\u2705 [OpenFront Channel] Channel updated with new tokens:", channel.id);
|
||||
} catch (error) {
|
||||
console.error("\u{1F6A8} [OpenFront Channel] Failed to update channel tokens in database:", error);
|
||||
}
|
||||
console.log("\u2705 [OpenFront Channel] Returning fresh access token");
|
||||
return access_token;
|
||||
} else {
|
||||
console.log("\u2705 [OpenFront Channel] Token still valid, using existing access token");
|
||||
return platform.accessToken;
|
||||
}
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: platform.refreshToken
|
||||
});
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Token refresh failed:", errorText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
const { access_token } = await response.json();
|
||||
return access_token;
|
||||
}
|
||||
console.log("\u26A0\uFE0F [OpenFront Channel] No refresh token available, using existing access token");
|
||||
return platform.accessToken;
|
||||
};
|
||||
createOpenFrontClient = async (platform) => {
|
||||
@@ -1430,6 +1654,7 @@ async function searchOrdersFunction({
|
||||
searchEntry,
|
||||
after
|
||||
}) {
|
||||
console.log("fuckkk");
|
||||
const openFrontClient = await createOpenFrontClient2(platform);
|
||||
const gqlQuery = import_graphql_request3.gql`
|
||||
query SearchOrders($where: OrderWhereInput, $take: Int, $skip: Int) {
|
||||
@@ -1511,6 +1736,7 @@ async function searchOrdersFunction({
|
||||
take,
|
||||
skip
|
||||
});
|
||||
console.log("Orders from OpenFront:", orders);
|
||||
const transformedOrders = orders.map((order) => {
|
||||
const shippingAddress = order.shippingAddress || {};
|
||||
return {
|
||||
@@ -1531,22 +1757,28 @@ async function searchOrdersFunction({
|
||||
financialStatus: order.status,
|
||||
totalPrice: order.rawTotal ? (order.rawTotal / 100).toFixed(2) : "0.00",
|
||||
currency: order.currency?.code || "USD",
|
||||
lineItems: (order.lineItems || []).map((lineItem) => ({
|
||||
lineItemId: lineItem.id,
|
||||
name: lineItem.title,
|
||||
quantity: lineItem.quantity,
|
||||
image: getProductImageUrl2({ imagePath: lineItem.thumbnail, image: { url: lineItem.productVariant?.product?.thumbnail } }, platform.domain) || "",
|
||||
price: lineItem.moneyAmount ? (lineItem.moneyAmount.amount / 100).toFixed(2) : "0.00",
|
||||
variantId: lineItem.productVariant?.id || "",
|
||||
productId: lineItem.productVariant?.product?.id || "",
|
||||
sku: lineItem.sku || lineItem.productVariant?.sku || ""
|
||||
})),
|
||||
lineItems: (order.lineItems || []).map((lineItem) => {
|
||||
const productTitle = lineItem.productVariant?.product?.title || "";
|
||||
const variantTitle = lineItem.productVariant?.title || "";
|
||||
const combinedTitle = productTitle && variantTitle ? `${productTitle} - ${variantTitle}` : lineItem.title;
|
||||
return {
|
||||
lineItemId: lineItem.id,
|
||||
name: combinedTitle,
|
||||
quantity: lineItem.quantity,
|
||||
image: getProductImageUrl2({ imagePath: lineItem.thumbnail, image: { url: lineItem.productVariant?.product?.thumbnail } }, platform.domain) || "",
|
||||
price: lineItem.moneyAmount ? (lineItem.moneyAmount.amount / 100).toFixed(2) : "0.00",
|
||||
variantId: lineItem.productVariant?.id || "",
|
||||
productId: lineItem.productVariant?.product?.id || "",
|
||||
sku: lineItem.sku || lineItem.productVariant?.sku || ""
|
||||
};
|
||||
}),
|
||||
cartItems: [],
|
||||
fulfillments: [],
|
||||
note: "",
|
||||
cursor: Buffer.from((skip + orders.indexOf(order) + 1).toString()).toString("base64")
|
||||
};
|
||||
});
|
||||
console.log("Transformed orders:", transformedOrders);
|
||||
const hasNextPage = skip + take < ordersCount;
|
||||
const endCursor = hasNextPage ? Buffer.from((skip + take).toString()).toString("base64") : null;
|
||||
return {
|
||||
@@ -1719,10 +1951,10 @@ async function oAuthCallbackFunction3({
|
||||
}
|
||||
const { access_token, refresh_token, expires_in } = await response.json();
|
||||
return {
|
||||
access_token,
|
||||
refresh_token,
|
||||
expires_in,
|
||||
expires_at: new Date(Date.now() + expires_in * 1e3)
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token,
|
||||
expiresIn: expires_in,
|
||||
tokenExpiresAt: new Date(Date.now() + expires_in * 1e3).toISOString()
|
||||
};
|
||||
}
|
||||
async function createOrderWebhookHandler({
|
||||
@@ -1730,41 +1962,51 @@ async function createOrderWebhookHandler({
|
||||
event,
|
||||
headers
|
||||
}) {
|
||||
const signature = headers["x-openfront-webhook-signature"];
|
||||
const signature = headers["x-openfront-webhook-signature"] || headers["X-OpenFront-Webhook-Signature"];
|
||||
if (!signature) {
|
||||
console.error("Missing webhook signature. Available headers:", Object.keys(headers));
|
||||
throw new Error("Missing webhook signature");
|
||||
}
|
||||
const lineItemsOutput = event.data?.orderLineItems?.map((item) => ({
|
||||
name: item.title,
|
||||
image: getProductImageUrl2(item.productVariant?.product?.productImages?.[0], platform.domain),
|
||||
price: item.unitPrice ? item.unitPrice / 100 : 0,
|
||||
// Convert from cents, handle null/undefined
|
||||
quantity: item.quantity || 0,
|
||||
productId: item.productVariant?.product?.id,
|
||||
variantId: item.productVariant?.id,
|
||||
sku: item.productVariant?.sku || "",
|
||||
lineItemId: item.id
|
||||
})) || [];
|
||||
const lineItemsOutput = event.data?.lineItems?.map((item) => {
|
||||
const productTitle = item.productVariant?.product?.title || "";
|
||||
const variantTitle = item.productVariant?.title || "";
|
||||
const combinedTitle = productTitle && variantTitle ? `${productTitle} - ${variantTitle}` : item.title;
|
||||
return {
|
||||
name: combinedTitle,
|
||||
image: getProductImageUrl2(item.productVariant?.product?.productImages?.[0], platform.domain) || item.thumbnail,
|
||||
price: item.moneyAmount?.amount ? item.moneyAmount.amount / 100 : 0,
|
||||
// Convert from cents to float
|
||||
quantity: item.quantity || 0,
|
||||
productId: item.productVariant?.product?.id?.toString(),
|
||||
variantId: item.productVariant?.id?.toString(),
|
||||
sku: item.productVariant?.sku || item.sku || "",
|
||||
lineItemId: item.id?.toString()
|
||||
};
|
||||
}) || [];
|
||||
const orderData = event.data;
|
||||
const shippingAddress = orderData.shippingAddress || {};
|
||||
return {
|
||||
orderId: orderData.id,
|
||||
orderName: orderData.orderNumber,
|
||||
email: orderData.customerEmail,
|
||||
firstName: orderData.firstName,
|
||||
lastName: orderData.lastName,
|
||||
streetAddress1: orderData.address1,
|
||||
streetAddress2: orderData.address2,
|
||||
city: orderData.city,
|
||||
state: orderData.state,
|
||||
zip: orderData.postalCode,
|
||||
country: orderData.countryCode,
|
||||
phone: orderData.phone,
|
||||
currency: orderData.currency?.code || "USD",
|
||||
totalPrice: orderData.total ? orderData.total / 100 : 0,
|
||||
// Convert from cents, handle null/undefined
|
||||
subTotalPrice: orderData.subtotal || orderData.total ? (orderData.subtotal || orderData.total) / 100 : 0,
|
||||
totalDiscounts: orderData.totalDiscounts ? orderData.totalDiscounts / 100 : 0,
|
||||
totalTax: orderData.totalTax ? orderData.totalTax / 100 : 0,
|
||||
orderId: orderData.id?.toString(),
|
||||
orderName: orderData.displayId ? `#${orderData.displayId}` : "",
|
||||
email: orderData.email || "",
|
||||
firstName: shippingAddress.firstName || "",
|
||||
lastName: shippingAddress.lastName || "",
|
||||
streetAddress1: shippingAddress.address1 || "",
|
||||
streetAddress2: shippingAddress.address2 || "",
|
||||
city: shippingAddress.city || "",
|
||||
state: shippingAddress.province || "",
|
||||
zip: shippingAddress.postalCode || "",
|
||||
country: shippingAddress.country?.iso2?.toUpperCase() || "",
|
||||
phone: shippingAddress.phone || "",
|
||||
currency: orderData.currency?.code?.toUpperCase() || "USD",
|
||||
totalPrice: orderData.rawTotal ? orderData.rawTotal / 100 : 0,
|
||||
// rawTotal is in cents, convert to float
|
||||
subTotalPrice: parseFloat(orderData.subtotal?.replace(/[$,]/g, "") || "0"),
|
||||
// Parse formatted string to float
|
||||
totalDiscounts: parseFloat(orderData.discount?.replace(/[$,]/g, "") || "0"),
|
||||
// Parse formatted string to float
|
||||
totalTax: parseFloat(orderData.tax?.replace(/[$,]/g, "") || "0"),
|
||||
// Parse formatted string to float
|
||||
status: "INPROCESS",
|
||||
linkOrder: true,
|
||||
matchOrder: true,
|
||||
@@ -1822,36 +2064,109 @@ async function addTrackingFunction2({
|
||||
});
|
||||
return result;
|
||||
}
|
||||
var import_graphql_request3, import_getBaseUrl, getFreshAccessToken2, createOpenFrontClient2, getProductImageUrl2, REQUIRED_SCOPES3;
|
||||
var import_graphql_request3, import_getBaseUrl, import_context2, getFreshAccessToken2, createOpenFrontClient2, getProductImageUrl2, REQUIRED_SCOPES3;
|
||||
var init_openfront2 = __esm({
|
||||
"features/integrations/shop/openfront.ts"() {
|
||||
"use strict";
|
||||
import_graphql_request3 = require("graphql-request");
|
||||
import_getBaseUrl = require("@/features/dashboard/lib/getBaseUrl");
|
||||
import_context2 = require("@/features/keystone/context");
|
||||
getFreshAccessToken2 = async (platform) => {
|
||||
if (platform.tokenExpiresAt && platform.refreshToken) {
|
||||
const expiresAt = typeof platform.tokenExpiresAt === "string" ? new Date(platform.tokenExpiresAt) : platform.tokenExpiresAt;
|
||||
if (expiresAt > /* @__PURE__ */ new Date()) {
|
||||
console.log("\u{1F504} [OpenFront Shop] getFreshAccessToken called");
|
||||
console.log("\u{1F504} [OpenFront Shop] Platform domain:", platform.domain);
|
||||
console.log("\u{1F504} [OpenFront Shop] Actual access token:", platform.accessToken);
|
||||
const shops = await import_context2.keystoneContext.sudo().query.Shop.findMany({
|
||||
where: {
|
||||
domain: { equals: platform.domain },
|
||||
accessToken: { equals: platform.accessToken }
|
||||
},
|
||||
query: "id refreshToken tokenExpiresAt platform { appKey appSecret }"
|
||||
});
|
||||
if (!shops || shops.length === 0) {
|
||||
console.log("\u26A0\uFE0F [OpenFront Shop] No matching shop found in database");
|
||||
return platform.accessToken;
|
||||
}
|
||||
const shop = shops[0];
|
||||
console.log("\u{1F504} [OpenFront Shop] Found shop:", shop.id);
|
||||
console.log("\u{1F504} [OpenFront Shop] Has refresh token:", !!shop.refreshToken);
|
||||
console.log("\u{1F504} [OpenFront Shop] Token expires at:", shop.tokenExpiresAt);
|
||||
console.log("\u{1F504} [OpenFront Shop] Has appKey:", !!shop.platform?.appKey);
|
||||
console.log("\u{1F504} [OpenFront Shop] Has appSecret:", !!shop.platform?.appSecret);
|
||||
console.log("\u{1F504} [OpenFront Shop] Actual refresh token:", shop.refreshToken);
|
||||
if (shop.refreshToken) {
|
||||
console.log("\u{1F504} [OpenFront Shop] Refresh token found, checking if refresh needed");
|
||||
let shouldRefresh = false;
|
||||
if (shop.tokenExpiresAt) {
|
||||
const expiresAt = typeof shop.tokenExpiresAt === "string" ? new Date(shop.tokenExpiresAt) : shop.tokenExpiresAt;
|
||||
const now = /* @__PURE__ */ new Date();
|
||||
shouldRefresh = expiresAt <= now;
|
||||
console.log("\u{1F504} [OpenFront Shop] Token expiry check:");
|
||||
console.log("\u{1F504} [OpenFront Shop] - Expires at:", expiresAt.toISOString());
|
||||
console.log("\u{1F504} [OpenFront Shop] - Current time:", now.toISOString());
|
||||
console.log("\u{1F504} [OpenFront Shop] - Should refresh:", shouldRefresh);
|
||||
} else {
|
||||
shouldRefresh = true;
|
||||
console.log("\u{1F504} [OpenFront Shop] No expiry info found, assuming refresh needed");
|
||||
}
|
||||
if (shouldRefresh) {
|
||||
console.log("\u{1F504} [OpenFront Shop] Starting token refresh process...");
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
console.log("\u{1F504} [OpenFront Shop] Token URL:", tokenUrl);
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: shop.refreshToken,
|
||||
client_id: shop.platform?.appKey || "",
|
||||
client_secret: shop.platform?.appSecret || ""
|
||||
});
|
||||
console.log("\u{1F504} [OpenFront Shop] Refresh request params:");
|
||||
console.log("\u{1F504} [OpenFront Shop] - grant_type: refresh_token");
|
||||
console.log("\u{1F504} [OpenFront Shop] - client_id:", shop.platform?.appKey || "NOT_SET");
|
||||
console.log("\u{1F504} [OpenFront Shop] - client_secret:", shop.platform?.appSecret ? "SET" : "NOT_SET");
|
||||
console.log("\u{1F504} [OpenFront Shop] - refresh_token:", shop.refreshToken ? "SET" : "NOT_SET");
|
||||
console.log("\u{1F504} [OpenFront Shop] Making refresh token request...");
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData
|
||||
});
|
||||
console.log("\u{1F504} [OpenFront Shop] Refresh response status:", response.status);
|
||||
console.log("\u{1F504} [OpenFront Shop] Refresh response ok:", response.ok);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("\u{1F6A8} [OpenFront Shop] Token refresh failed:", errorText);
|
||||
console.error("\u{1F6A8} [OpenFront Shop] Response status:", response.status);
|
||||
console.error("\u{1F6A8} [OpenFront Shop] Response statusText:", response.statusText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
const tokenData = await response.json();
|
||||
console.log("\u{1F504} [OpenFront Shop] Token refresh response received");
|
||||
console.log("\u{1F504} [OpenFront Shop] - Has access_token:", !!tokenData.access_token);
|
||||
console.log("\u{1F504} [OpenFront Shop] - Has refresh_token:", !!tokenData.refresh_token);
|
||||
console.log("\u{1F504} [OpenFront Shop] - Expires in:", tokenData.expires_in, "seconds");
|
||||
const { access_token, refresh_token, expires_in } = tokenData;
|
||||
console.log("\u{1F504} [OpenFront Shop] Updating tokens in database...");
|
||||
try {
|
||||
console.log("\u{1F504} [OpenFront Shop] Updating shop:", shop.id);
|
||||
await import_context2.keystoneContext.sudo().query.Shop.updateOne({
|
||||
where: { id: shop.id },
|
||||
data: {
|
||||
accessToken: access_token,
|
||||
...refresh_token && { refreshToken: refresh_token },
|
||||
...expires_in && { tokenExpiresAt: new Date(Date.now() + expires_in * 1e3) }
|
||||
}
|
||||
});
|
||||
console.log("\u2705 [OpenFront Shop] Shop updated with new tokens:", shop.id);
|
||||
} catch (error) {
|
||||
console.error("\u{1F6A8} [OpenFront Shop] Failed to update shop tokens in database:", error);
|
||||
}
|
||||
console.log("\u2705 [OpenFront Shop] Returning fresh access token");
|
||||
return access_token;
|
||||
} else {
|
||||
console.log("\u2705 [OpenFront Shop] Token still valid, using existing access token");
|
||||
return platform.accessToken;
|
||||
}
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: platform.refreshToken
|
||||
});
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Token refresh failed:", errorText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
const { access_token } = await response.json();
|
||||
return access_token;
|
||||
}
|
||||
console.log("\u26A0\uFE0F [OpenFront Shop] No refresh token available, using existing access token");
|
||||
return platform.accessToken;
|
||||
};
|
||||
createOpenFrontClient2 = async (platform) => {
|
||||
@@ -3721,6 +4036,7 @@ async function placeMultipleOrders({ ids, query }) {
|
||||
zip,
|
||||
country,
|
||||
phone,
|
||||
currency,
|
||||
user,
|
||||
shop,
|
||||
orderId: shopOrderId,
|
||||
@@ -3738,7 +4054,8 @@ async function placeMultipleOrders({ ids, query }) {
|
||||
state,
|
||||
zip,
|
||||
country,
|
||||
phone
|
||||
phone,
|
||||
currency
|
||||
shop {
|
||||
domain
|
||||
accessToken
|
||||
@@ -3826,7 +4143,8 @@ async function placeMultipleOrders({ ids, query }) {
|
||||
zip,
|
||||
country,
|
||||
phone,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
currency
|
||||
},
|
||||
notes: ""
|
||||
});
|
||||
@@ -6103,8 +6421,8 @@ var ShopPlatform = (0, import_core18.list)({
|
||||
label: "App Credentials",
|
||||
description: "Adding these fields will enable this platform to be installed as an app by users",
|
||||
fields: {
|
||||
appKey: (0, import_fields17.text)({ validation: { isRequired: true } }),
|
||||
appSecret: (0, import_fields17.text)({ validation: { isRequired: true } }),
|
||||
appKey: (0, import_fields17.text)(),
|
||||
appSecret: (0, import_fields17.text)(),
|
||||
callbackUrl: (0, import_fields17.virtual)({
|
||||
field: import_core18.graphql.field({
|
||||
type: import_core18.graphql.String,
|
||||
@@ -6204,8 +6522,8 @@ var ChannelPlatform = (0, import_core19.list)({
|
||||
label: "App Credentials",
|
||||
description: "Adding these fields will enable this platform to be installed as an app by users.",
|
||||
fields: {
|
||||
appKey: (0, import_fields18.text)({ validation: { isRequired: true } }),
|
||||
appSecret: (0, import_fields18.text)({ validation: { isRequired: true } }),
|
||||
appKey: (0, import_fields18.text)(),
|
||||
appSecret: (0, import_fields18.text)(),
|
||||
callbackUrl: (0, import_fields18.virtual)({
|
||||
field: import_core19.graphql.field({
|
||||
type: import_core19.graphql.String,
|
||||
@@ -7363,7 +7681,6 @@ function statelessSessions({
|
||||
async get({ context }) {
|
||||
if (!context?.req) return;
|
||||
const authHeader = context.req.headers.authorization;
|
||||
console.log("\u{1F511} AUTH HEADER:", authHeader);
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
const accessToken = authHeader.replace("Bearer ", "");
|
||||
console.log("\u{1F511} ACCESS TOKEN:", accessToken);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -153,7 +153,7 @@ openship/
|
||||
|
||||
## Documentation
|
||||
|
||||
For comprehensive technical documentation, see [docs.openship.org/openship/ecommerce](https://docs.openship.org/openship/ecommerce) which covers:
|
||||
For comprehensive technical documentation, see [docs.openship.org/docs/openship/ecommerce](https://docs.openship.org/docs/openship/ecommerce) which covers:
|
||||
- Complete integration guides
|
||||
- API reference and operations
|
||||
- Custom shop and channel development
|
||||
@@ -176,15 +176,6 @@ Openship is production-ready and can be deployed to:
|
||||
- Webhook reliability and retry mechanisms
|
||||
- Monitoring and alerting for failed orders
|
||||
|
||||
## Foundation
|
||||
|
||||
This project is built on [next-keystone-starter](https://github.com/junaid33/next-keystone-starter), which provides:
|
||||
- Modern full-stack architecture
|
||||
- KeystoneJS 6 integration with Next.js 15
|
||||
- Authentication and session management
|
||||
- Database setup with Prisma
|
||||
- Development tooling and configuration
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please see our contributing guidelines for details on:
|
||||
@@ -199,7 +190,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: Check our comprehensive documentation at [docs.openship.org/openship/ecommerce](https://docs.openship.org/openship/ecommerce)
|
||||
- **Documentation**: Check our comprehensive documentation at [docs.openship.org/docs/openship/ecommerce](https://docs.openship.org/docs/openship/ecommerce)
|
||||
- **Issues**: Report bugs and feature requests on GitHub Issues
|
||||
- **Community**: Join our community discussions
|
||||
- **Enterprise**: Contact us for enterprise support and custom integrations
|
||||
@@ -207,4 +198,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
||||
---
|
||||
|
||||
**Openship** - Order Routing Platform
|
||||
Built with Next.js 15 and KeystoneJS 6 on [next-keystone-starter](https://github.com/junaid33/next-keystone-starter)
|
||||
Built with Next.js 15 and KeystoneJS 6
|
||||
@@ -38,6 +38,9 @@ async function generateOAuthState(platformId: string, type: 'shop' | 'channel'):
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('🚀 OAuth callback endpoint called');
|
||||
console.log('🔍 Request URL:', request.url);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// Get OAuth parameters
|
||||
@@ -51,6 +54,16 @@ export async function GET(request: NextRequest) {
|
||||
const autoCreate = searchParams.get('auto_create') === 'true';
|
||||
const appName = searchParams.get('app_name');
|
||||
const clientId = searchParams.get('client_id');
|
||||
|
||||
console.log('🔍 OAuth callback parameters:');
|
||||
console.log(' - code:', code ? `${code.substring(0, 10)}...` : 'MISSING');
|
||||
console.log(' - state:', state ? `${state.substring(0, 20)}...` : 'MISSING');
|
||||
console.log(' - shop:', shop);
|
||||
console.log(' - error:', error);
|
||||
console.log(' - errorDescription:', errorDescription);
|
||||
console.log(' - autoCreate:', autoCreate);
|
||||
console.log(' - appName:', appName);
|
||||
console.log(' - clientId:', clientId);
|
||||
|
||||
// Handle OAuth errors
|
||||
if (error) {
|
||||
@@ -168,14 +181,24 @@ export async function GET(request: NextRequest) {
|
||||
redirectUri: `${await getBaseUrl()}/api/oauth/callback`
|
||||
});
|
||||
|
||||
console.log('🔍 Token exchange result type:', typeof tokenResult);
|
||||
console.log('🔍 Token exchange result:', typeof tokenResult === 'string' ? `${tokenResult.substring(0, 10)}...` : tokenResult);
|
||||
|
||||
// Handle both old string format and new object format
|
||||
let accessToken, refreshToken, tokenExpiresAt;
|
||||
if (typeof tokenResult === 'string') {
|
||||
console.log('📝 Using legacy string token format');
|
||||
accessToken = tokenResult;
|
||||
} else {
|
||||
accessToken = tokenResult.access_token;
|
||||
refreshToken = tokenResult.refresh_token;
|
||||
tokenExpiresAt = tokenResult.expires_at;
|
||||
console.log('📝 Using new object token format');
|
||||
// OpenFront returns camelCase field names
|
||||
accessToken = tokenResult.accessToken;
|
||||
refreshToken = tokenResult.refreshToken;
|
||||
tokenExpiresAt = tokenResult.tokenExpiresAt;
|
||||
console.log('🔑 Token details:');
|
||||
console.log(' - accessToken present:', !!accessToken);
|
||||
console.log(' - refreshToken present:', !!refreshToken);
|
||||
console.log(' - tokenExpiresAt present:', !!tokenExpiresAt);
|
||||
}
|
||||
|
||||
const baseUrl = await getBaseUrl();
|
||||
@@ -185,6 +208,12 @@ export async function GET(request: NextRequest) {
|
||||
const endpoint = redirectAppType === 'channel' ? 'channels' : 'shops';
|
||||
const createParam = redirectAppType === 'channel' ? 'showCreateChannel' : 'showCreateShop';
|
||||
|
||||
console.log('🎯 Building redirect URL:');
|
||||
console.log(' - baseUrl:', baseUrl);
|
||||
console.log(' - redirectAppType:', redirectAppType);
|
||||
console.log(' - endpoint:', endpoint);
|
||||
console.log(' - createParam:', createParam);
|
||||
|
||||
const redirectUrl = new URL(`${baseUrl}/dashboard/platform/${endpoint}`);
|
||||
redirectUrl.searchParams.set(createParam, 'true');
|
||||
redirectUrl.searchParams.set('domain', shop ?? '');
|
||||
@@ -193,13 +222,24 @@ export async function GET(request: NextRequest) {
|
||||
redirectUrl.searchParams.set('client_secret', stateData.client_secret);
|
||||
redirectUrl.searchParams.set('app_name', stateData.app_name);
|
||||
redirectUrl.searchParams.set('adapter_slug', stateData.adapter_slug);
|
||||
|
||||
console.log('🔑 Adding optional tokens to redirect:');
|
||||
if (refreshToken) {
|
||||
console.log(' - Adding refreshToken to URL params');
|
||||
redirectUrl.searchParams.set('refreshToken', refreshToken);
|
||||
} else {
|
||||
console.log(' - No refreshToken to add');
|
||||
}
|
||||
if (tokenExpiresAt) {
|
||||
redirectUrl.searchParams.set('tokenExpiresAt', tokenExpiresAt.toISOString());
|
||||
console.log(' - Adding tokenExpiresAt to URL params:', tokenExpiresAt);
|
||||
// tokenExpiresAt is already a string from OpenFront adapter
|
||||
redirectUrl.searchParams.set('tokenExpiresAt', tokenExpiresAt);
|
||||
} else {
|
||||
console.log(' - No tokenExpiresAt to add');
|
||||
}
|
||||
|
||||
console.log('🎯 Final redirect URL:', redirectUrl.toString());
|
||||
|
||||
return NextResponse.redirect(redirectUrl.toString());
|
||||
}
|
||||
|
||||
@@ -254,7 +294,7 @@ export async function GET(request: NextRequest) {
|
||||
accessToken = tokenResult;
|
||||
} else {
|
||||
// New format - object with both tokens
|
||||
accessToken = tokenResult.access_token;
|
||||
accessToken = tokenResult.accessToken;
|
||||
}
|
||||
|
||||
// Redirect to shops page with params
|
||||
@@ -264,8 +304,12 @@ export async function GET(request: NextRequest) {
|
||||
redirectUrl.searchParams.set('accessToken', accessToken);
|
||||
if (typeof tokenResult === 'object') {
|
||||
// Include additional token data for new implementations
|
||||
redirectUrl.searchParams.set('refreshToken', tokenResult.refresh_token);
|
||||
redirectUrl.searchParams.set('tokenExpiresAt', tokenResult.expires_at.toISOString());
|
||||
if (tokenResult.refreshToken) {
|
||||
redirectUrl.searchParams.set('refreshToken', tokenResult.refreshToken);
|
||||
}
|
||||
if (tokenResult.tokenExpiresAt) {
|
||||
redirectUrl.searchParams.set('tokenExpiresAt', tokenResult.tokenExpiresAt);
|
||||
}
|
||||
}
|
||||
redirectUrl.searchParams.set('domain', shop ?? '');
|
||||
|
||||
@@ -288,7 +332,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Exchange code for access token
|
||||
accessToken = await handleChannelOAuthCallback({
|
||||
const channelTokenResult = await handleChannelOAuthCallback({
|
||||
platform,
|
||||
code,
|
||||
shop: shop || undefined,
|
||||
@@ -298,11 +342,29 @@ export async function GET(request: NextRequest) {
|
||||
redirectUri: `${baseUrl}/api/oauth/callback`, // Single callback URL
|
||||
});
|
||||
|
||||
// Handle both old string format and new object format for backward compatibility
|
||||
if (typeof channelTokenResult === 'string') {
|
||||
// Legacy format - just access token
|
||||
accessToken = channelTokenResult;
|
||||
} else {
|
||||
// New format - object with both tokens
|
||||
accessToken = channelTokenResult.accessToken;
|
||||
}
|
||||
|
||||
// Redirect to channels page with params
|
||||
const redirectUrl = new URL(`${baseUrl}/dashboard/platform/channels`);
|
||||
redirectUrl.searchParams.set('showCreateChannel', 'true');
|
||||
redirectUrl.searchParams.set('platform', platformId);
|
||||
redirectUrl.searchParams.set('accessToken', accessToken);
|
||||
if (typeof channelTokenResult === 'object') {
|
||||
// Include additional token data for new implementations
|
||||
if (channelTokenResult.refreshToken) {
|
||||
redirectUrl.searchParams.set('refreshToken', channelTokenResult.refreshToken);
|
||||
}
|
||||
if (channelTokenResult.tokenExpiresAt) {
|
||||
redirectUrl.searchParams.set('tokenExpiresAt', channelTokenResult.tokenExpiresAt);
|
||||
}
|
||||
}
|
||||
redirectUrl.searchParams.set('domain', shop ?? '');
|
||||
|
||||
return NextResponse.redirect(redirectUrl.toString());
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface DateTimePickerProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function DateTimePicker({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Pick a date and time",
|
||||
className,
|
||||
disabled = false,
|
||||
}: DateTimePickerProps) {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
value ? new Date(value) : undefined
|
||||
);
|
||||
const [time, setTime] = React.useState<string>(
|
||||
value ? format(new Date(value), "HH:mm") : ""
|
||||
);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleDateSelect = (selectedDate: Date | undefined) => {
|
||||
if (selectedDate) {
|
||||
const newDate = new Date(selectedDate);
|
||||
if (time) {
|
||||
const [hours, minutes] = time.split(":");
|
||||
newDate.setHours(parseInt(hours), parseInt(minutes));
|
||||
}
|
||||
setDate(newDate);
|
||||
const isoString = newDate.toISOString().slice(0, 16); // YYYY-MM-DDTHH:mm format
|
||||
onChange?.(isoString);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTimeChange = (newTime: string) => {
|
||||
setTime(newTime);
|
||||
if (date && newTime) {
|
||||
const [hours, minutes] = newTime.split(":");
|
||||
const newDate = new Date(date);
|
||||
newDate.setHours(parseInt(hours), parseInt(minutes));
|
||||
setDate(newDate);
|
||||
const isoString = newDate.toISOString().slice(0, 16); // YYYY-MM-DDTHH:mm format
|
||||
onChange?.(isoString);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setDate(undefined);
|
||||
setTime("");
|
||||
onChange?.("");
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-start text-left font-normal",
|
||||
!date && "text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{date ? (
|
||||
format(date, "PPP 'at' HH:mm")
|
||||
) : (
|
||||
<span>{placeholder}</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<div className="p-3 space-y-3">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={handleDateSelect}
|
||||
initialFocus
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="time-input" className="text-sm font-medium">
|
||||
Time
|
||||
</Label>
|
||||
<Input
|
||||
id="time-input"
|
||||
type="time"
|
||||
value={time}
|
||||
onChange={(e) => handleTimeChange(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleClear}
|
||||
className="flex-1"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex-1"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -444,7 +444,7 @@ const MultipleSelector = ({
|
||||
inputRef?.current?.focus()
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-wrap gap-1 max-h-20 overflow-y-auto">
|
||||
{selected.map((option) => {
|
||||
return (
|
||||
<div
|
||||
@@ -545,7 +545,7 @@ const MultipleSelector = ({
|
||||
>
|
||||
{open && (
|
||||
<CommandList
|
||||
className="bg-popover text-popover-foreground shadow-lg outline-hidden"
|
||||
className="bg-popover text-popover-foreground shadow-lg outline-hidden max-h-48 overflow-auto"
|
||||
onMouseLeave={() => {
|
||||
setOnScrollbar(false)
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GraphQLClient, gql } from "graphql-request";
|
||||
import { keystoneContext } from '@/features/keystone/context';
|
||||
|
||||
interface OpenFrontPlatform {
|
||||
domain: string;
|
||||
@@ -36,46 +37,132 @@ interface DeleteWebhookArgs {
|
||||
|
||||
// Helper function to get fresh access token with proper OAuth 2.0 flow
|
||||
const getFreshAccessToken = async (platform: OpenFrontPlatform) => {
|
||||
// Check if we have local access token expiry information
|
||||
if (platform.tokenExpiresAt && platform.refreshToken) {
|
||||
const expiresAt = typeof platform.tokenExpiresAt === 'string'
|
||||
? new Date(platform.tokenExpiresAt)
|
||||
: platform.tokenExpiresAt;
|
||||
console.log('🔄 [OpenFront Channel] getFreshAccessToken called');
|
||||
console.log('🔄 [OpenFront Channel] Platform domain:', platform.domain);
|
||||
console.log('🔄 [OpenFront Channel] Actual access token:', platform.accessToken);
|
||||
|
||||
// Get channel with OAuth credentials from database
|
||||
const channels = await keystoneContext.sudo().query.Channel.findMany({
|
||||
where: {
|
||||
domain: { equals: platform.domain },
|
||||
accessToken: { equals: platform.accessToken }
|
||||
},
|
||||
query: 'id refreshToken tokenExpiresAt platform { appKey appSecret }'
|
||||
});
|
||||
|
||||
if (!channels || channels.length === 0) {
|
||||
console.log('⚠️ [OpenFront Channel] No matching channel found in database');
|
||||
return platform.accessToken;
|
||||
}
|
||||
|
||||
const channel = channels[0];
|
||||
console.log('🔄 [OpenFront Channel] Found channel:', channel.id);
|
||||
console.log('🔄 [OpenFront Channel] Has refresh token:', !!channel.refreshToken);
|
||||
console.log('🔄 [OpenFront Channel] Token expires at:', channel.tokenExpiresAt);
|
||||
console.log('🔄 [OpenFront Channel] Has appKey:', !!channel.platform?.appKey);
|
||||
console.log('🔄 [OpenFront Channel] Has appSecret:', !!channel.platform?.appSecret);
|
||||
console.log('🔄 [OpenFront Channel] Actual refresh token:', channel.refreshToken);
|
||||
|
||||
// If we have a refresh token, check if we need to refresh
|
||||
if (channel.refreshToken) {
|
||||
console.log('🔄 [OpenFront Channel] Refresh token found, checking if refresh needed');
|
||||
|
||||
// If access token hasn't expired yet, use it
|
||||
if (expiresAt > new Date()) {
|
||||
return platform.accessToken;
|
||||
// Check if access token has expired (if we have expiry info)
|
||||
let shouldRefresh = false;
|
||||
|
||||
if (channel.tokenExpiresAt) {
|
||||
const expiresAt = typeof channel.tokenExpiresAt === 'string'
|
||||
? new Date(channel.tokenExpiresAt)
|
||||
: channel.tokenExpiresAt;
|
||||
|
||||
const now = new Date();
|
||||
shouldRefresh = expiresAt <= now;
|
||||
|
||||
console.log('🔄 [OpenFront Channel] Token expiry check:');
|
||||
console.log('🔄 [OpenFront Channel] - Expires at:', expiresAt.toISOString());
|
||||
console.log('🔄 [OpenFront Channel] - Current time:', now.toISOString());
|
||||
console.log('🔄 [OpenFront Channel] - Should refresh:', shouldRefresh);
|
||||
} else {
|
||||
// If no expiry info, assume token needs refresh
|
||||
shouldRefresh = true;
|
||||
console.log('🔄 [OpenFront Channel] No expiry info found, assuming refresh needed');
|
||||
}
|
||||
|
||||
// Use refresh token to get new access token
|
||||
if (shouldRefresh) {
|
||||
console.log('🔄 [OpenFront Channel] Starting token refresh process...');
|
||||
|
||||
// Use refresh token to get new access token
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
console.log('🔄 [OpenFront Channel] Token URL:', tokenUrl);
|
||||
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: platform.refreshToken,
|
||||
refresh_token: channel.refreshToken,
|
||||
client_id: channel.platform?.appKey || "",
|
||||
client_secret: channel.platform?.appSecret || "",
|
||||
});
|
||||
|
||||
console.log('🔄 [OpenFront Channel] Refresh request params:');
|
||||
console.log('🔄 [OpenFront Channel] - grant_type: refresh_token');
|
||||
console.log('🔄 [OpenFront Channel] - client_id:', channel.platform?.appKey || "NOT_SET");
|
||||
console.log('🔄 [OpenFront Channel] - client_secret:', channel.platform?.appSecret ? "SET" : "NOT_SET");
|
||||
console.log('🔄 [OpenFront Channel] - refresh_token:', channel.refreshToken ? "SET" : "NOT_SET");
|
||||
|
||||
console.log('🔄 [OpenFront Channel] Making refresh token request...');
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
console.log('🔄 [OpenFront Channel] Refresh response status:', response.status);
|
||||
console.log('🔄 [OpenFront Channel] Refresh response ok:', response.ok);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Token refresh failed:', errorText);
|
||||
console.error('🚨 [OpenFront Channel] Token refresh failed:', errorText);
|
||||
console.error('🚨 [OpenFront Channel] Response status:', response.status);
|
||||
console.error('🚨 [OpenFront Channel] Response statusText:', response.statusText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const { access_token } = await response.json();
|
||||
const tokenData = await response.json();
|
||||
console.log('🔄 [OpenFront Channel] Token refresh response received');
|
||||
console.log('🔄 [OpenFront Channel] - Has access_token:', !!tokenData.access_token);
|
||||
console.log('🔄 [OpenFront Channel] - Has refresh_token:', !!tokenData.refresh_token);
|
||||
console.log('🔄 [OpenFront Channel] - Expires in:', tokenData.expires_in, 'seconds');
|
||||
|
||||
// TODO: Update stored access token and expiry in database
|
||||
// This would require updating the shop/channel record with new tokens
|
||||
const { access_token, refresh_token, expires_in } = tokenData;
|
||||
|
||||
// Update stored access token and expiry in database
|
||||
console.log('🔄 [OpenFront Channel] Updating tokens in database...');
|
||||
try {
|
||||
console.log('🔄 [OpenFront Channel] Updating channel:', channel.id);
|
||||
await keystoneContext.sudo().query.Channel.updateOne({
|
||||
where: { id: channel.id },
|
||||
data: {
|
||||
accessToken: access_token,
|
||||
...(refresh_token && { refreshToken: refresh_token }),
|
||||
...(expires_in && { tokenExpiresAt: new Date(Date.now() + (expires_in * 1000)) })
|
||||
}
|
||||
});
|
||||
console.log('✅ [OpenFront Channel] Channel updated with new tokens:', channel.id);
|
||||
} catch (error) {
|
||||
console.error('🚨 [OpenFront Channel] Failed to update channel tokens in database:', error);
|
||||
// Continue with the request even if database update fails
|
||||
}
|
||||
|
||||
console.log('✅ [OpenFront Channel] Returning fresh access token');
|
||||
return access_token;
|
||||
} else {
|
||||
// Token hasn't expired yet, use existing one
|
||||
console.log('✅ [OpenFront Channel] Token still valid, using existing access token');
|
||||
return platform.accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
// If no refresh capability, just use the access token as-is
|
||||
// If no refresh token, just use the access token as-is
|
||||
console.log('⚠️ [OpenFront Channel] No refresh token available, using existing access token');
|
||||
return platform.accessToken;
|
||||
};
|
||||
|
||||
@@ -229,7 +316,7 @@ export async function getProductFunction({
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
|
||||
const gqlQuery = gql`
|
||||
query GetChannelProduct($productId: ID!, $variantId: ID) {
|
||||
query GetChannelProduct($productId: ID!, $variantWhere: ProductVariantWhereInput) {
|
||||
product(where: { id: $productId }) {
|
||||
id
|
||||
title
|
||||
@@ -240,7 +327,7 @@ export async function getProductFunction({
|
||||
}
|
||||
imagePath
|
||||
}
|
||||
productVariants(where: $variantId ? { id: { equals: $variantId } } : {}) {
|
||||
productVariants(where: $variantWhere) {
|
||||
id
|
||||
title
|
||||
sku
|
||||
@@ -260,7 +347,7 @@ export async function getProductFunction({
|
||||
|
||||
const { product } = await openFrontClient.request(gqlQuery, {
|
||||
productId,
|
||||
variantId,
|
||||
variantWhere: variantId ? { id: { equals: variantId } } : {},
|
||||
}) as any;
|
||||
|
||||
if (!product || product.status !== "published") {
|
||||
@@ -306,108 +393,197 @@ export async function createPurchaseFunction({
|
||||
}) {
|
||||
console.log(`🛒 OpenFront Channel: Creating purchase with ${cartItems.length} items`);
|
||||
console.log(`🚚 OpenFront Channel: Ship to: ${shipping?.firstName} ${shipping?.lastName}`);
|
||||
console.log(`📦 OpenFront Channel: Full shipping data:`, JSON.stringify(shipping, null, 2));
|
||||
console.log(`📦 OpenFront Channel: Platform data:`, JSON.stringify(platform, null, 2));
|
||||
console.log(`📦 OpenFront Channel: Cart items:`, JSON.stringify(cartItems, null, 2));
|
||||
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
|
||||
// Generate unique purchase ID
|
||||
const purchaseId = `PO-OF-${Date.now()}`;
|
||||
const orderNumber = `#${purchaseId}`;
|
||||
|
||||
// Calculate total price
|
||||
const totalPrice = cartItems.reduce((sum: number, item: any) => {
|
||||
return sum + (parseFloat(item.price) * item.quantity);
|
||||
}, 0);
|
||||
|
||||
// Create fulfillment order in OpenFront
|
||||
const createOrderMutation = gql`
|
||||
mutation CreateFulfillmentOrder($data: OrderCreateInput!) {
|
||||
createOrder(data: $data) {
|
||||
id
|
||||
orderNumber
|
||||
total
|
||||
status
|
||||
orderLineItems {
|
||||
try {
|
||||
// Step 1: Get region data with currency ID and taxRate (like storefront does)
|
||||
const currencyCode = (shipping?.currency || "USD").toLowerCase();
|
||||
const getRegionQuery = gql`
|
||||
query GetRegion($currencyCode: String!) {
|
||||
regions(where: { currency: { code: { equals: $currencyCode } } }) {
|
||||
id
|
||||
title
|
||||
quantity
|
||||
unitPrice
|
||||
productVariant {
|
||||
taxRate
|
||||
currency {
|
||||
id
|
||||
title
|
||||
sku
|
||||
product {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { regions } = await openFrontClient.request(getRegionQuery, {
|
||||
currencyCode: currencyCode,
|
||||
}) as any;
|
||||
|
||||
const region = regions[0];
|
||||
if (!region) {
|
||||
throw new Error(`No region found for currency: ${currencyCode}`);
|
||||
}
|
||||
|
||||
// Step 2: Create cart (following storefront flow)
|
||||
console.log("🛒 [OpenFront Channel] Creating cart with region:", region.id);
|
||||
|
||||
const { createCart: cart } = await openFrontClient.request(gql`
|
||||
mutation CreateCart($data: CartCreateInput!) {
|
||||
createCart(data: $data) {
|
||||
id
|
||||
region {
|
||||
id
|
||||
currency {
|
||||
id
|
||||
title
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
// Prepare order data
|
||||
const orderData = {
|
||||
orderNumber,
|
||||
customerEmail: shipping?.email || "fulfillment@openship.org",
|
||||
firstName: shipping?.firstName || "Fulfillment",
|
||||
lastName: shipping?.lastName || "Order",
|
||||
address1: shipping?.streetAddress1 || "",
|
||||
address2: shipping?.streetAddress2 || "",
|
||||
city: shipping?.city || "",
|
||||
state: shipping?.state || "",
|
||||
postalCode: shipping?.zip || "",
|
||||
countryCode: shipping?.country || "US",
|
||||
phone: shipping?.phone || "",
|
||||
total: Math.round(totalPrice * 100), // Convert to cents
|
||||
subtotal: Math.round(totalPrice * 100),
|
||||
status: "pending_fulfillment",
|
||||
orderLineItems: {
|
||||
create: cartItems.map((item: any) => ({
|
||||
title: item.name || `Product ${item.variantId}`,
|
||||
quantity: item.quantity,
|
||||
unitPrice: Math.round(parseFloat(item.price) * 100), // Convert to cents
|
||||
productVariant: { connect: { id: item.variantId } }
|
||||
}))
|
||||
`, {
|
||||
data: {
|
||||
region: { connect: { id: region.id } },
|
||||
email: shipping?.email || `order-${Date.now()}@openship.generated`
|
||||
}
|
||||
};
|
||||
|
||||
const result = await openFrontClient.request(createOrderMutation, {
|
||||
data: orderData,
|
||||
}) as any;
|
||||
|
||||
const order = result.createOrder;
|
||||
console.log("✅ [OpenFront Channel] Cart created with ID:", cart.id);
|
||||
|
||||
console.log(`📧 OpenFront Channel: Fulfillment Order Created: ${orderNumber}`);
|
||||
console.log(`💰 OpenFront Channel: Total: $${totalPrice.toFixed(2)}`);
|
||||
// Step 3: Add line items to cart (this computes unitPrice/total automatically)
|
||||
const lineItemsToCreate = [];
|
||||
for (const item of cartItems) {
|
||||
lineItemsToCreate.push({
|
||||
productVariant: { connect: { id: item.variantId } },
|
||||
quantity: item.quantity
|
||||
});
|
||||
}
|
||||
|
||||
// Process line items for response
|
||||
const processedLineItems = order.orderLineItems.map((item: any) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
console.log("📦 [OpenFront Channel] Adding", lineItemsToCreate.length, "line items to cart");
|
||||
|
||||
await openFrontClient.request(gql`
|
||||
mutation AddLineItemsToCart($cartId: ID!, $data: CartUpdateInput!) {
|
||||
updateActiveCart(cartId: $cartId, data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`, {
|
||||
cartId: cart.id,
|
||||
data: {
|
||||
lineItems: {
|
||||
create: lineItemsToCreate
|
||||
}
|
||||
}
|
||||
}) as any;
|
||||
|
||||
console.log("✅ [OpenFront Channel] Line items added to cart");
|
||||
|
||||
// Step 4: Create addresses and add to cart
|
||||
console.log("📍 [OpenFront Channel] Creating shipping address");
|
||||
|
||||
const { createAddress: shippingAddr } = await openFrontClient.request(gql`
|
||||
mutation CreateAddress($data: AddressCreateInput!) {
|
||||
createAddress(data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`, {
|
||||
data: {
|
||||
firstName: shipping?.firstName || "Guest",
|
||||
lastName: shipping?.lastName || "Customer",
|
||||
address1: shipping?.address1 || "123 Default St",
|
||||
city: shipping?.city || "Default City",
|
||||
province: shipping?.state || "NY",
|
||||
postalCode: shipping?.zip || "10001",
|
||||
phone: shipping?.phone || "",
|
||||
country: {
|
||||
connect: {
|
||||
iso2: (shipping?.country || "US").toLowerCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as any;
|
||||
|
||||
console.log("✅ [OpenFront Channel] Shipping address created:", shippingAddr.id);
|
||||
|
||||
// Step 5: Update cart with addresses
|
||||
await openFrontClient.request(gql`
|
||||
mutation UpdateCartAddresses($cartId: ID!, $data: CartUpdateInput!) {
|
||||
updateActiveCart(cartId: $cartId, data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`, {
|
||||
cartId: cart.id,
|
||||
data: {
|
||||
shippingAddress: { connect: { id: shippingAddr.id } },
|
||||
billingAddress: { connect: { id: shippingAddr.id } }
|
||||
}
|
||||
}) as any;
|
||||
|
||||
console.log("✅ [OpenFront Channel] Cart updated with addresses");
|
||||
|
||||
// Step 6: Complete cart to create order (like storefront placeOrder)
|
||||
console.log("🎯 [OpenFront Channel] Completing cart to create order");
|
||||
|
||||
console.log("🎯 [OpenFront Channel] Calling completeActiveCart for cart:", cart.id);
|
||||
|
||||
const completeResult = await openFrontClient.request(gql`
|
||||
mutation CompleteActiveCart($cartId: ID!) {
|
||||
completeActiveCart(cartId: $cartId)
|
||||
}
|
||||
`, {
|
||||
cartId: cart.id
|
||||
}) as any;
|
||||
|
||||
console.log("🔍 [OpenFront Channel] completeActiveCart result:", JSON.stringify(completeResult, null, 2));
|
||||
|
||||
const order = completeResult.completeActiveCart;
|
||||
if (!order?.id) {
|
||||
throw new Error("Failed to complete cart - no order created");
|
||||
}
|
||||
|
||||
console.log("🔍 [OpenFront Channel] COMPLETE ORDER RESULT:", JSON.stringify(order, null, 2));
|
||||
console.log("🎉 [OpenFront Channel] Order created successfully:", order.id);
|
||||
|
||||
// Process line items from cart items (since order doesn't include line items)
|
||||
const processedLineItems = cartItems.map((item: any) => ({
|
||||
id: item.variantId,
|
||||
title: item.name || `Product ${item.variantId}`,
|
||||
quantity: item.quantity,
|
||||
variantId: item.productVariant.id,
|
||||
productId: item.productVariant.product.id
|
||||
variantId: item.variantId,
|
||||
}));
|
||||
|
||||
// Return success response
|
||||
// Return in the same format as Shopify channel
|
||||
return {
|
||||
purchaseId: order.id,
|
||||
orderNumber: order.orderNumber,
|
||||
totalPrice: (order.total / 100).toFixed(2),
|
||||
orderNumber: `#${order.displayId}`,
|
||||
totalPrice: order.total,
|
||||
invoiceUrl: `https://${platform.domain}/admin/orders/${order.id}`,
|
||||
lineItems: processedLineItems,
|
||||
status: "processing"
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('OpenFront Channel: Purchase creation failed:', error);
|
||||
} catch (error: any) {
|
||||
console.error('🚨 OpenFront Channel: Purchase creation failed:', error);
|
||||
|
||||
// Return error response
|
||||
if (error.response?.errors) {
|
||||
console.error('🚨 GraphQL Errors:');
|
||||
error.response.errors.forEach((err: any, i: number) => {
|
||||
console.error(` ${i + 1}. ${err.message}`);
|
||||
if (err.path) {
|
||||
console.error(` Path: ${err.path.join(' -> ')}`);
|
||||
}
|
||||
if (err.extensions) {
|
||||
console.error(` Extensions:`, err.extensions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return error response with detailed error info
|
||||
return {
|
||||
purchaseId,
|
||||
orderNumber,
|
||||
totalPrice: totalPrice.toFixed(2),
|
||||
purchaseId: null,
|
||||
orderNumber: null,
|
||||
totalPrice: "0.00",
|
||||
invoiceUrl: null,
|
||||
lineItems: cartItems.map((item: any) => ({
|
||||
id: `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
@@ -422,7 +598,7 @@ export async function createPurchaseFunction({
|
||||
}
|
||||
}
|
||||
|
||||
// Function to create webhook for channel events
|
||||
// Function to create webhook for channel events - simplified to update user's webhook URL
|
||||
export async function createWebhookFunction({
|
||||
platform,
|
||||
endpoint,
|
||||
@@ -432,46 +608,128 @@ export async function createWebhookFunction({
|
||||
endpoint: string;
|
||||
events: string[];
|
||||
}) {
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
console.log('🪝 [OpenFront Channel] createWebhookFunction called');
|
||||
console.log('🪝 [OpenFront Channel] Platform domain:', platform.domain);
|
||||
console.log('🪝 [OpenFront Channel] Endpoint URL:', endpoint);
|
||||
console.log('🪝 [OpenFront Channel] Events received:', events);
|
||||
console.log('🪝 [OpenFront Channel] Events type:', typeof events, 'Array?', Array.isArray(events));
|
||||
console.log('🪝 [OpenFront Channel] Platform data:', JSON.stringify(platform, null, 2));
|
||||
|
||||
const createWebhookMutation = gql`
|
||||
mutation CreateChannelWebhookEndpoint($data: WebhookEndpointCreateInput!) {
|
||||
createWebhookEndpoint(data: $data) {
|
||||
id
|
||||
url
|
||||
events
|
||||
isActive
|
||||
secret
|
||||
// Map Openship channel events to OpenFront events
|
||||
const eventMap: Record<string, string> = {
|
||||
ORDER_CREATED: "order.created",
|
||||
ORDER_CANCELLED: "order.cancelled",
|
||||
TRACKING_CREATED: "fulfillment.created",
|
||||
};
|
||||
|
||||
const openFrontEvents = events.map(event => {
|
||||
const mapped = eventMap[event] || event;
|
||||
console.log(`🪝 [OpenFront Channel] Mapping: ${event} -> ${mapped}`);
|
||||
return mapped;
|
||||
});
|
||||
console.log('🪝 [OpenFront Channel] Final mapped events array:', openFrontEvents);
|
||||
console.log('🪝 [OpenFront Channel] Joined events string:', openFrontEvents.join(", "));
|
||||
|
||||
try {
|
||||
console.log('🪝 [OpenFront Channel] Creating OpenFront GraphQL client...');
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
console.log('✅ [OpenFront Channel] GraphQL client created successfully');
|
||||
|
||||
// Get current user from the access token
|
||||
console.log('🪝 [OpenFront Channel] Fetching current user...');
|
||||
const getUserQuery = gql`
|
||||
query GetCurrentUser {
|
||||
authenticatedItem {
|
||||
... on User {
|
||||
id
|
||||
email
|
||||
orderWebhookUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
console.log('🪝 [OpenFront Channel] Executing getUserQuery...');
|
||||
const { authenticatedItem: user } = await openFrontClient.request(getUserQuery) as any;
|
||||
console.log('🪝 [OpenFront Channel] getUserQuery result:', JSON.stringify(user, null, 2));
|
||||
|
||||
if (!user) {
|
||||
console.error('🚨 [OpenFront Channel] User not authenticated - authenticatedItem is null/undefined');
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
console.log('✅ [OpenFront Channel] User authenticated successfully');
|
||||
console.log('🪝 [OpenFront Channel] User ID:', user.id);
|
||||
console.log('🪝 [OpenFront Channel] User email:', user.email);
|
||||
console.log('🪝 [OpenFront Channel] Current webhook URL:', user.orderWebhookUrl);
|
||||
|
||||
// Update the user's webhook URL using updateActiveUser (bypasses access restrictions)
|
||||
console.log('🪝 [OpenFront Channel] Updating user webhook URL...');
|
||||
const updateUserMutation = gql`
|
||||
mutation UpdateActiveUserWebhookUrl($data: UserUpdateProfileInput!) {
|
||||
updateActiveUser(data: $data) {
|
||||
id
|
||||
orderWebhookUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
console.log('🪝 [OpenFront Channel] Update mutation variables:');
|
||||
console.log('🪝 [OpenFront Channel] - User ID:', user.id);
|
||||
console.log('🪝 [OpenFront Channel] - New webhook URL:', endpoint);
|
||||
|
||||
const result = await openFrontClient.request(updateUserMutation, {
|
||||
data: { orderWebhookUrl: endpoint }
|
||||
}) as any;
|
||||
|
||||
console.log('🪝 [OpenFront Channel] Update mutation result:', JSON.stringify(result, null, 2));
|
||||
|
||||
const updatedUser = result.updateActiveUser;
|
||||
console.log('✅ [OpenFront Channel] User webhook URL updated successfully');
|
||||
console.log('🪝 [OpenFront Channel] Updated user ID:', updatedUser.id);
|
||||
console.log('🪝 [OpenFront Channel] Updated webhook URL:', updatedUser.orderWebhookUrl);
|
||||
|
||||
const webhookResponse = {
|
||||
webhooks: [{
|
||||
id: `user-${updatedUser.id}`,
|
||||
callbackUrl: updatedUser.orderWebhookUrl,
|
||||
topic: openFrontEvents.join(", "), // Join array into comma-separated string
|
||||
format: "JSON",
|
||||
createdAt: new Date().toISOString()
|
||||
}],
|
||||
webhookId: `user-${updatedUser.id}`
|
||||
};
|
||||
|
||||
console.log('🪝 [OpenFront Channel] Final webhook response:', JSON.stringify(webhookResponse, null, 2));
|
||||
return webhookResponse;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('🚨 [OpenFront Channel] createWebhookFunction failed:', error);
|
||||
console.error('🚨 [OpenFront Channel] Error message:', error.message);
|
||||
console.error('🚨 [OpenFront Channel] Error stack:', error.stack);
|
||||
|
||||
if (error.response) {
|
||||
console.error('🚨 [OpenFront Channel] GraphQL response:', JSON.stringify(error.response, null, 2));
|
||||
|
||||
if (error.response.errors) {
|
||||
console.error('🚨 [OpenFront Channel] GraphQL errors:');
|
||||
error.response.errors.forEach((err: any, i: number) => {
|
||||
console.error(`🚨 [OpenFront Channel] ${i + 1}. ${err.message}`);
|
||||
if (err.path) {
|
||||
console.error(`🚨 [OpenFront Channel] Path: ${err.path.join(' -> ')}`);
|
||||
}
|
||||
if (err.extensions) {
|
||||
console.error(`🚨 [OpenFront Channel] Extensions:`, err.extensions);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Map channel events to OpenFront events
|
||||
const eventMap: Record<string, string> = {
|
||||
PURCHASE_CREATED: "order.created",
|
||||
PURCHASE_SHIPPED: "fulfillment.shipped",
|
||||
INVENTORY_UPDATED: "inventory.updated",
|
||||
};
|
||||
|
||||
const openFrontEvents = events.map(event => eventMap[event] || event);
|
||||
|
||||
const result = await openFrontClient.request(createWebhookMutation, {
|
||||
data: {
|
||||
url: endpoint,
|
||||
events: openFrontEvents,
|
||||
isActive: true,
|
||||
},
|
||||
}) as any;
|
||||
|
||||
const webhook = result.createWebhookEndpoint;
|
||||
|
||||
return {
|
||||
webhooks: [webhook],
|
||||
webhookId: webhook.id
|
||||
};
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to delete webhook
|
||||
// Function to delete webhook - clear user's webhook URL
|
||||
export async function deleteWebhookFunction({
|
||||
platform,
|
||||
webhookId,
|
||||
@@ -481,22 +739,28 @@ export async function deleteWebhookFunction({
|
||||
}) {
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
|
||||
const deleteWebhookMutation = gql`
|
||||
mutation DeleteChannelWebhookEndpoint($where: WebhookEndpointWhereUniqueInput!) {
|
||||
deleteWebhookEndpoint(where: $where) {
|
||||
// Extract user ID from webhookId (format: "user-{userId}")
|
||||
const userId = webhookId.replace('user-', '');
|
||||
|
||||
// Clear the user's webhook URL
|
||||
const updateUserMutation = gql`
|
||||
mutation ClearUserWebhookUrl($where: UserWhereUniqueInput!, $data: UserUpdateInput!) {
|
||||
updateUser(where: $where, data: $data) {
|
||||
id
|
||||
orderWebhookUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await openFrontClient.request(deleteWebhookMutation, {
|
||||
where: { id: webhookId },
|
||||
const result = await openFrontClient.request(updateUserMutation, {
|
||||
where: { id: userId },
|
||||
data: { orderWebhookUrl: null }
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Function to get webhooks
|
||||
// Function to get webhooks - get user's webhook URL
|
||||
export async function getWebhooksFunction({
|
||||
platform,
|
||||
}: {
|
||||
@@ -504,34 +768,41 @@ export async function getWebhooksFunction({
|
||||
}) {
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
|
||||
const query = gql`
|
||||
query GetChannelWebhookEndpoints {
|
||||
webhookEndpoints(where: { isActive: { equals: true } }) {
|
||||
id
|
||||
url
|
||||
events
|
||||
isActive
|
||||
createdAt
|
||||
// Map OpenFront events back to Openship channel events
|
||||
const eventMap: Record<string, string> = {
|
||||
"order.created": "PURCHASE_CREATED",
|
||||
"fulfillment.created": "PURCHASE_SHIPPED",
|
||||
};
|
||||
|
||||
// Get current user's webhook URL
|
||||
const getUserQuery = gql`
|
||||
query GetCurrentUser {
|
||||
authenticatedItem {
|
||||
... on User {
|
||||
id
|
||||
email
|
||||
orderWebhookUrl
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { webhookEndpoints } = await openFrontClient.request(query) as any;
|
||||
const { authenticatedItem: user } = await openFrontClient.request(getUserQuery) as any;
|
||||
|
||||
if (!user || !user.orderWebhookUrl) {
|
||||
return { webhooks: [] };
|
||||
}
|
||||
|
||||
// Map OpenFront events back to channel events
|
||||
const eventMap: Record<string, string> = {
|
||||
"order.created": "PURCHASE_CREATED",
|
||||
"fulfillment.shipped": "PURCHASE_SHIPPED",
|
||||
"inventory.updated": "INVENTORY_UPDATED",
|
||||
};
|
||||
|
||||
const webhooks = webhookEndpoints.map((webhook: any) => ({
|
||||
id: webhook.id,
|
||||
callbackUrl: webhook.url,
|
||||
topic: webhook.events.map((event: string) => eventMap[event] || event),
|
||||
// For OpenFront channels, we support both PURCHASE_CREATED and PURCHASE_SHIPPED
|
||||
// Since we store a single webhook URL on the user, we return both events as supported
|
||||
const webhooks = [{
|
||||
id: `user-${user.id}`,
|
||||
callbackUrl: user.orderWebhookUrl,
|
||||
topic: ["PURCHASE_CREATED", "PURCHASE_SHIPPED"], // These are the channel events we support
|
||||
format: "JSON",
|
||||
createdAt: webhook.createdAt,
|
||||
}));
|
||||
createdAt: user.createdAt
|
||||
}];
|
||||
|
||||
return { webhooks };
|
||||
}
|
||||
@@ -702,9 +973,15 @@ export async function oAuthCallbackFunction({
|
||||
throw new Error(`Failed to exchange OAuth code for access token: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const { access_token } = await response.json();
|
||||
const { access_token, refresh_token, expires_in } = await response.json();
|
||||
|
||||
return access_token; // Return just the token, as expected by OpenShip
|
||||
// Return OAuth response with refresh token support
|
||||
return {
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token,
|
||||
expiresIn: expires_in,
|
||||
tokenExpiresAt: new Date(Date.now() + (expires_in * 1000)).toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// Required OAuth scopes for OpenFront channel integration
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GraphQLClient, gql } from "graphql-request";
|
||||
import { getBaseUrl } from '@/features/dashboard/lib/getBaseUrl';
|
||||
import { keystoneContext } from '@/features/keystone/context';
|
||||
|
||||
interface OpenFrontPlatform {
|
||||
domain: string;
|
||||
@@ -58,48 +59,132 @@ interface WebhookEventArgs {
|
||||
|
||||
// Helper function to get fresh access token with proper OAuth 2.0 flow
|
||||
const getFreshAccessToken = async (platform: OpenFrontPlatform) => {
|
||||
// Check if we have local access token expiry information
|
||||
if (platform.tokenExpiresAt && platform.refreshToken) {
|
||||
const expiresAt = typeof platform.tokenExpiresAt === 'string'
|
||||
? new Date(platform.tokenExpiresAt)
|
||||
: platform.tokenExpiresAt;
|
||||
|
||||
// If access token hasn't expired yet, use it
|
||||
if (expiresAt > new Date()) {
|
||||
return platform.accessToken;
|
||||
}
|
||||
|
||||
|
||||
// Use refresh token to get new access token
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: platform.refreshToken,
|
||||
});
|
||||
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Token refresh failed:', errorText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const { access_token } = await response.json();
|
||||
|
||||
// TODO: Update stored access token and expiry in database
|
||||
// This would require updating the shop/channel record with new tokens
|
||||
|
||||
return access_token;
|
||||
console.log('🔄 [OpenFront Shop] getFreshAccessToken called');
|
||||
console.log('🔄 [OpenFront Shop] Platform domain:', platform.domain);
|
||||
console.log('🔄 [OpenFront Shop] Actual access token:', platform.accessToken);
|
||||
|
||||
// Get shop with OAuth credentials from database
|
||||
const shops = await keystoneContext.sudo().query.Shop.findMany({
|
||||
where: {
|
||||
domain: { equals: platform.domain },
|
||||
accessToken: { equals: platform.accessToken }
|
||||
},
|
||||
query: 'id refreshToken tokenExpiresAt platform { appKey appSecret }'
|
||||
});
|
||||
|
||||
if (!shops || shops.length === 0) {
|
||||
console.log('⚠️ [OpenFront Shop] No matching shop found in database');
|
||||
return platform.accessToken;
|
||||
}
|
||||
|
||||
const shop = shops[0];
|
||||
console.log('🔄 [OpenFront Shop] Found shop:', shop.id);
|
||||
console.log('🔄 [OpenFront Shop] Has refresh token:', !!shop.refreshToken);
|
||||
console.log('🔄 [OpenFront Shop] Token expires at:', shop.tokenExpiresAt);
|
||||
console.log('🔄 [OpenFront Shop] Has appKey:', !!shop.platform?.appKey);
|
||||
console.log('🔄 [OpenFront Shop] Has appSecret:', !!shop.platform?.appSecret);
|
||||
console.log('🔄 [OpenFront Shop] Actual refresh token:', shop.refreshToken);
|
||||
|
||||
// If no refresh capability, just use the access token as-is
|
||||
// If we have a refresh token, check if we need to refresh
|
||||
if (shop.refreshToken) {
|
||||
console.log('🔄 [OpenFront Shop] Refresh token found, checking if refresh needed');
|
||||
|
||||
// Check if access token has expired (if we have expiry info)
|
||||
let shouldRefresh = false;
|
||||
|
||||
if (shop.tokenExpiresAt) {
|
||||
const expiresAt = typeof shop.tokenExpiresAt === 'string'
|
||||
? new Date(shop.tokenExpiresAt)
|
||||
: shop.tokenExpiresAt;
|
||||
|
||||
const now = new Date();
|
||||
shouldRefresh = expiresAt <= now;
|
||||
|
||||
console.log('🔄 [OpenFront Shop] Token expiry check:');
|
||||
console.log('🔄 [OpenFront Shop] - Expires at:', expiresAt.toISOString());
|
||||
console.log('🔄 [OpenFront Shop] - Current time:', now.toISOString());
|
||||
console.log('🔄 [OpenFront Shop] - Should refresh:', shouldRefresh);
|
||||
} else {
|
||||
// If no expiry info, assume token needs refresh
|
||||
shouldRefresh = true;
|
||||
console.log('🔄 [OpenFront Shop] No expiry info found, assuming refresh needed');
|
||||
}
|
||||
|
||||
if (shouldRefresh) {
|
||||
console.log('🔄 [OpenFront Shop] Starting token refresh process...');
|
||||
|
||||
// Use refresh token to get new access token
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
console.log('🔄 [OpenFront Shop] Token URL:', tokenUrl);
|
||||
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: shop.refreshToken,
|
||||
client_id: shop.platform?.appKey || "",
|
||||
client_secret: shop.platform?.appSecret || "",
|
||||
});
|
||||
|
||||
console.log('🔄 [OpenFront Shop] Refresh request params:');
|
||||
console.log('🔄 [OpenFront Shop] - grant_type: refresh_token');
|
||||
console.log('🔄 [OpenFront Shop] - client_id:', shop.platform?.appKey || "NOT_SET");
|
||||
console.log('🔄 [OpenFront Shop] - client_secret:', shop.platform?.appSecret ? "SET" : "NOT_SET");
|
||||
console.log('🔄 [OpenFront Shop] - refresh_token:', shop.refreshToken ? "SET" : "NOT_SET");
|
||||
|
||||
console.log('🔄 [OpenFront Shop] Making refresh token request...');
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
console.log('🔄 [OpenFront Shop] Refresh response status:', response.status);
|
||||
console.log('🔄 [OpenFront Shop] Refresh response ok:', response.ok);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('🚨 [OpenFront Shop] Token refresh failed:', errorText);
|
||||
console.error('🚨 [OpenFront Shop] Response status:', response.status);
|
||||
console.error('🚨 [OpenFront Shop] Response statusText:', response.statusText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const tokenData = await response.json();
|
||||
console.log('🔄 [OpenFront Shop] Token refresh response received');
|
||||
console.log('🔄 [OpenFront Shop] - Has access_token:', !!tokenData.access_token);
|
||||
console.log('🔄 [OpenFront Shop] - Has refresh_token:', !!tokenData.refresh_token);
|
||||
console.log('🔄 [OpenFront Shop] - Expires in:', tokenData.expires_in, 'seconds');
|
||||
|
||||
const { access_token, refresh_token, expires_in } = tokenData;
|
||||
|
||||
// Update stored access token and expiry in database
|
||||
console.log('🔄 [OpenFront Shop] Updating tokens in database...');
|
||||
try {
|
||||
console.log('🔄 [OpenFront Shop] Updating shop:', shop.id);
|
||||
await keystoneContext.sudo().query.Shop.updateOne({
|
||||
where: { id: shop.id },
|
||||
data: {
|
||||
accessToken: access_token,
|
||||
...(refresh_token && { refreshToken: refresh_token }),
|
||||
...(expires_in && { tokenExpiresAt: new Date(Date.now() + (expires_in * 1000)) })
|
||||
}
|
||||
});
|
||||
console.log('✅ [OpenFront Shop] Shop updated with new tokens:', shop.id);
|
||||
} catch (error) {
|
||||
console.error('🚨 [OpenFront Shop] Failed to update shop tokens in database:', error);
|
||||
// Continue with the request even if database update fails
|
||||
}
|
||||
|
||||
console.log('✅ [OpenFront Shop] Returning fresh access token');
|
||||
return access_token;
|
||||
} else {
|
||||
// Token hasn't expired yet, use existing one
|
||||
console.log('✅ [OpenFront Shop] Token still valid, using existing access token');
|
||||
return platform.accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
// If no refresh token, just use the access token as-is
|
||||
console.log('⚠️ [OpenFront Shop] No refresh token available, using existing access token');
|
||||
return platform.accessToken;
|
||||
};
|
||||
|
||||
@@ -370,6 +455,7 @@ export async function searchOrdersFunction({
|
||||
searchEntry: string;
|
||||
after?: string;
|
||||
}) {
|
||||
console.log("fuckkk")
|
||||
const openFrontClient = await createOpenFrontClient(platform);
|
||||
|
||||
const gqlQuery = gql`
|
||||
@@ -459,6 +545,8 @@ export async function searchOrdersFunction({
|
||||
skip,
|
||||
}) as any;
|
||||
|
||||
console.log("Orders from OpenFront:", orders);
|
||||
|
||||
// Transform orders to Openship format
|
||||
const transformedOrders = orders.map((order: any) => {
|
||||
const shippingAddress = order.shippingAddress || {};
|
||||
@@ -481,16 +569,23 @@ export async function searchOrdersFunction({
|
||||
financialStatus: order.status,
|
||||
totalPrice: order.rawTotal ? (order.rawTotal / 100).toFixed(2) : "0.00",
|
||||
currency: order.currency?.code || "USD",
|
||||
lineItems: (order.lineItems || []).map((lineItem: any) => ({
|
||||
lineItemId: lineItem.id,
|
||||
name: lineItem.title,
|
||||
quantity: lineItem.quantity,
|
||||
image: getProductImageUrl({ imagePath: lineItem.thumbnail, image: { url: lineItem.productVariant?.product?.thumbnail } }, platform.domain) || "",
|
||||
price: lineItem.moneyAmount ? (lineItem.moneyAmount.amount / 100).toFixed(2) : "0.00",
|
||||
variantId: lineItem.productVariant?.id || "",
|
||||
productId: lineItem.productVariant?.product?.id || "",
|
||||
sku: lineItem.sku || lineItem.productVariant?.sku || "",
|
||||
})),
|
||||
lineItems: (order.lineItems || []).map((lineItem: any) => {
|
||||
// Combine product title and variant title like in channel search
|
||||
const productTitle = lineItem.productVariant?.product?.title || '';
|
||||
const variantTitle = lineItem.productVariant?.title || '';
|
||||
const combinedTitle = productTitle && variantTitle ? `${productTitle} - ${variantTitle}` : lineItem.title;
|
||||
|
||||
return {
|
||||
lineItemId: lineItem.id,
|
||||
name: combinedTitle,
|
||||
quantity: lineItem.quantity,
|
||||
image: getProductImageUrl({ imagePath: lineItem.thumbnail, image: { url: lineItem.productVariant?.product?.thumbnail } }, platform.domain) || "",
|
||||
price: lineItem.moneyAmount ? (lineItem.moneyAmount.amount / 100).toFixed(2) : "0.00",
|
||||
variantId: lineItem.productVariant?.id || "",
|
||||
productId: lineItem.productVariant?.product?.id || "",
|
||||
sku: lineItem.sku || lineItem.productVariant?.sku || "",
|
||||
};
|
||||
}),
|
||||
cartItems: [],
|
||||
fulfillments: [],
|
||||
note: "",
|
||||
@@ -498,6 +593,8 @@ export async function searchOrdersFunction({
|
||||
};
|
||||
});
|
||||
|
||||
console.log("Transformed orders:", transformedOrders);
|
||||
|
||||
const hasNextPage = skip + take < ordersCount;
|
||||
const endCursor = hasNextPage ? Buffer.from((skip + take).toString()).toString('base64') : null;
|
||||
|
||||
@@ -750,12 +847,12 @@ export async function oAuthCallbackFunction({
|
||||
|
||||
const { access_token, refresh_token, expires_in } = await response.json();
|
||||
|
||||
// Return OAuth token data for proper storage
|
||||
// Return OAuth response with refresh token support
|
||||
return {
|
||||
access_token,
|
||||
refresh_token,
|
||||
expires_in,
|
||||
expires_at: new Date(Date.now() + (expires_in * 1000))
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token,
|
||||
expiresIn: expires_in,
|
||||
tokenExpiresAt: new Date(Date.now() + (expires_in * 1000)).toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -776,16 +873,23 @@ export async function createOrderWebhookHandler({
|
||||
}
|
||||
|
||||
// Transform OpenFront order to Openship format
|
||||
const lineItemsOutput = event.data?.lineItems?.map((item: any) => ({
|
||||
name: item.title,
|
||||
image: getProductImageUrl(item.productVariant?.product?.productImages?.[0], platform.domain) || item.thumbnail,
|
||||
price: item.moneyAmount?.amount ? (item.moneyAmount.amount / 100) : 0, // Convert from cents to float
|
||||
quantity: item.quantity || 0,
|
||||
productId: item.productVariant?.product?.id?.toString(),
|
||||
variantId: item.productVariant?.id?.toString(),
|
||||
sku: item.productVariant?.sku || item.sku || "",
|
||||
lineItemId: item.id?.toString(),
|
||||
})) || [];
|
||||
const lineItemsOutput = event.data?.lineItems?.map((item: any) => {
|
||||
// Combine product title and variant title like in channel search
|
||||
const productTitle = item.productVariant?.product?.title || '';
|
||||
const variantTitle = item.productVariant?.title || '';
|
||||
const combinedTitle = productTitle && variantTitle ? `${productTitle} - ${variantTitle}` : item.title;
|
||||
|
||||
return {
|
||||
name: combinedTitle,
|
||||
image: getProductImageUrl(item.productVariant?.product?.productImages?.[0], platform.domain) || item.thumbnail,
|
||||
price: item.moneyAmount?.amount ? (item.moneyAmount.amount / 100) : 0, // Convert from cents to float
|
||||
quantity: item.quantity || 0,
|
||||
productId: item.productVariant?.product?.id?.toString(),
|
||||
variantId: item.productVariant?.id?.toString(),
|
||||
sku: item.productVariant?.sku || item.sku || "",
|
||||
lineItemId: item.id?.toString(),
|
||||
};
|
||||
}) || [];
|
||||
|
||||
// Return Keystone-ready order data
|
||||
const orderData = event.data;
|
||||
|
||||
@@ -52,7 +52,6 @@ export function statelessSessions({
|
||||
|
||||
// Check for OAuth Bearer token authentication
|
||||
const authHeader = context.req.headers.authorization;
|
||||
console.log('🔑 AUTH HEADER:', authHeader);
|
||||
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
const accessToken = authHeader.replace("Bearer ", "");
|
||||
|
||||
@@ -44,6 +44,7 @@ export async function placeMultipleOrders({ ids, query }: { ids: string[]; query
|
||||
zip,
|
||||
country,
|
||||
phone,
|
||||
currency,
|
||||
user,
|
||||
shop,
|
||||
orderId: shopOrderId,
|
||||
@@ -61,7 +62,8 @@ export async function placeMultipleOrders({ ids, query }: { ids: string[]; query
|
||||
state,
|
||||
zip,
|
||||
country,
|
||||
phone
|
||||
phone,
|
||||
currency
|
||||
shop {
|
||||
domain
|
||||
accessToken
|
||||
@@ -155,6 +157,7 @@ export async function placeMultipleOrders({ ids, query }: { ids: string[]; query
|
||||
country,
|
||||
phone,
|
||||
email: user.email,
|
||||
currency,
|
||||
},
|
||||
notes: "",
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ export const ChannelPlatform = list({
|
||||
description:
|
||||
"Adding these fields will enable this platform to be installed as an app by users.",
|
||||
fields: {
|
||||
appKey: text({ validation: { isRequired: true } }),
|
||||
appSecret: text({ validation: { isRequired: true } }),
|
||||
appKey: text(),
|
||||
appSecret: text(),
|
||||
callbackUrl: virtual({
|
||||
field: graphql.field({
|
||||
type: graphql.String,
|
||||
|
||||
@@ -24,8 +24,8 @@ export const ShopPlatform = list({
|
||||
description:
|
||||
"Adding these fields will enable this platform to be installed as an app by users",
|
||||
fields: {
|
||||
appKey: text({ validation: { isRequired: true } }),
|
||||
appSecret: text({ validation: { isRequired: true } }),
|
||||
appKey: text(),
|
||||
appSecret: text(),
|
||||
callbackUrl: virtual({
|
||||
field: graphql.field({
|
||||
type: graphql.String,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Copy } from "lucide-react";
|
||||
import {
|
||||
@@ -15,7 +16,9 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import MultipleSelector, { Option } from "@/components/ui/multiselect";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { XIcon, ChevronDown } from "lucide-react";
|
||||
import { DateTimePicker } from "@/components/ui/datetime-picker";
|
||||
import { createApiKey } from "../actions/getApiKeys";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
@@ -37,7 +40,12 @@ function generateApiKeyToken(): string {
|
||||
return prefix + result;
|
||||
}
|
||||
|
||||
// Define API key scopes as options for MultipleSelector
|
||||
export interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Define API key scopes as options
|
||||
const scopeOptions: Option[] = [
|
||||
{ value: "read_orders", label: "read_orders" },
|
||||
{ value: "write_orders", label: "write_orders" },
|
||||
@@ -60,6 +68,169 @@ const scopeOptions: Option[] = [
|
||||
{ value: "write_users", label: "write_users" },
|
||||
];
|
||||
|
||||
function ScopesMultiSelect({
|
||||
value = [],
|
||||
onChange,
|
||||
options = [],
|
||||
placeholder = "Select scopes for this API key",
|
||||
isDisabled = false,
|
||||
}: {
|
||||
value?: Option[];
|
||||
onChange?: (options: Option[]) => void;
|
||||
options?: Option[];
|
||||
placeholder?: string;
|
||||
isDisabled?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const commandRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
commandRef.current &&
|
||||
!commandRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(option: Option) => {
|
||||
if (!value.some((v) => v.value === option.value)) {
|
||||
const newValue = [...value, option];
|
||||
onChange?.(newValue);
|
||||
}
|
||||
setInputValue("");
|
||||
},
|
||||
[value, onChange]
|
||||
);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
(optionToRemove: Option) => {
|
||||
const newValue = value.filter((v) => v.value !== optionToRemove.value);
|
||||
onChange?.(newValue);
|
||||
},
|
||||
[value, onChange]
|
||||
);
|
||||
|
||||
const handleClearAll = useCallback(() => {
|
||||
onChange?.([]);
|
||||
}, [onChange]);
|
||||
|
||||
// Filter out selected options and by input value
|
||||
const filteredOptions = options.filter(
|
||||
(option) => !value.some((v) => v.value === option.value) &&
|
||||
option.label.toLowerCase().includes(inputValue.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={commandRef}>
|
||||
<div
|
||||
className={cn(
|
||||
"border-input ring-offset-background focus-within:ring-ring flex min-h-11 w-full items-center justify-between rounded-md border bg-transparent px-4 py-2 focus-within:ring-2 focus-within:ring-offset-2 text-left",
|
||||
isDisabled && "cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isDisabled) {
|
||||
setOpen(true);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-wrap gap-1 max-h-20 overflow-y-auto">
|
||||
{value.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="animate-fadeIn bg-background text-secondary-foreground relative inline-flex h-7 cursor-default items-center rounded-md border ps-2 pe-7 text-xs font-medium transition-all disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 max-w-full"
|
||||
>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap max-w-[200px]">{option.label}</span>
|
||||
{!isDisabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemove(option);
|
||||
}}
|
||||
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute -inset-y-px -end-px flex size-7 items-center justify-center rounded-e-md border border-transparent p-0 outline-hidden transition-[color,box-shadow] outline-none focus-visible:ring-[3px]"
|
||||
>
|
||||
<XIcon className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<input
|
||||
ref={inputRef}
|
||||
disabled={isDisabled}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex-1 bg-transparent outline-none",
|
||||
value.length === 0 ? "w-full" : "w-20"
|
||||
)}
|
||||
placeholder={value.length === 0 ? placeholder : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{value.length > 0 && !isDisabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClearAll();
|
||||
}}
|
||||
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex size-7 items-center justify-center rounded-md border border-transparent transition-[color,box-shadow] outline-none focus-visible:ring-[3px]"
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
{open && (
|
||||
<div className="absolute z-10 mt-1 w-full max-w-[400px]">
|
||||
<Command className="border-input rounded-md border bg-popover shadow-md">
|
||||
<CommandList className="max-h-48">
|
||||
<>
|
||||
{filteredOptions.length === 0 ? (
|
||||
<CommandEmpty className="py-6 text-center text-sm text-muted-foreground">
|
||||
{inputValue.length > 0 ? (
|
||||
<>No results found for "{inputValue}"</>
|
||||
) : (
|
||||
<>All options have been selected</>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
) : (
|
||||
<CommandGroup>
|
||||
{filteredOptions.map((option) => (
|
||||
<div className="px-1 py-0.5" key={option.value}>
|
||||
<div
|
||||
onClick={() => handleSelect(option)}
|
||||
className="relative cursor-pointer rounded-sm px-2 py-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<div className="flex-1 whitespace-normal break-words overflow-visible min-w-0 pr-2">
|
||||
{option.label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CreateApiKey() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -136,10 +307,62 @@ export function CreateApiKey() {
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(createdToken);
|
||||
toast.success("API key copied to clipboard!");
|
||||
// Modern browsers with clipboard API support
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(createdToken);
|
||||
toast.success("API key copied to clipboard!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback for older browsers or non-secure contexts
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = createdToken;
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-999999px";
|
||||
textArea.style.top = "-999999px";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
if (successful) {
|
||||
toast.success("API key copied to clipboard!");
|
||||
} else {
|
||||
throw new Error("Copy command failed");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to copy API key");
|
||||
console.error("Copy failed:", error);
|
||||
|
||||
// Final fallback - detect mobile and provide instructions
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
|
||||
if (isMobile) {
|
||||
// Mobile-specific fallback: create a temporary input and select it
|
||||
try {
|
||||
const input = document.createElement("input");
|
||||
input.value = createdToken;
|
||||
input.style.position = "fixed";
|
||||
input.style.left = "-999999px";
|
||||
input.style.top = "-999999px";
|
||||
document.body.appendChild(input);
|
||||
input.focus();
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
|
||||
// Show instructions to user
|
||||
toast.error("Please manually copy the selected text above");
|
||||
|
||||
// Clean up after a delay
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(input);
|
||||
}, 5000);
|
||||
} catch (mobileError) {
|
||||
toast.error("Copy failed. Please manually select and copy the API key");
|
||||
}
|
||||
} else {
|
||||
toast.error("Copy failed. Please manually select and copy the API key");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -194,18 +417,11 @@ export function CreateApiKey() {
|
||||
<Label>
|
||||
Scopes <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<MultipleSelector
|
||||
commandProps={{
|
||||
label: "Select API key scopes",
|
||||
}}
|
||||
<ScopesMultiSelect
|
||||
value={formData.scopes}
|
||||
defaultOptions={scopeOptions}
|
||||
placeholder="Select scopes for this API key"
|
||||
hideClearAllButton={false}
|
||||
hidePlaceholderWhenSelected={true}
|
||||
emptyIndicator={<p className="text-center text-sm">No scopes found</p>}
|
||||
options={scopeOptions}
|
||||
onChange={handleScopeChange}
|
||||
className="text-base"
|
||||
placeholder="Select scopes for this API key"
|
||||
/>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Select the minimum permissions needed for this API key
|
||||
@@ -214,11 +430,10 @@ export function CreateApiKey() {
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="expiresAt">Expiration (optional)</Label>
|
||||
<Input
|
||||
id="expiresAt"
|
||||
type="datetime-local"
|
||||
<DateTimePicker
|
||||
value={formData.expiresAt}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, expiresAt: e.target.value }))}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, expiresAt: value }))}
|
||||
placeholder="Set expiration date and time"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,7 +466,7 @@ export function CreateApiKey() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-mono truncate">
|
||||
<div className="text-sm font-mono truncate max-w-[200px]">
|
||||
{createdToken}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +59,10 @@ export function CreateChannelFromURL({ onChannelCreated, searchParams }: CreateC
|
||||
const [tokenExpiresAt, setTokenExpiresAt] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchParams || !searchParams.showCreateChannel) return;
|
||||
|
||||
if (!searchParams || !searchParams.showCreateChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
platform: urlPlatform,
|
||||
@@ -74,55 +77,58 @@ export function CreateChannelFromURL({ onChannelCreated, searchParams }: CreateC
|
||||
tokenExpiresAt: urlTokenExpiresAt
|
||||
} = searchParams;
|
||||
|
||||
if (!urlDomain) return;
|
||||
|
||||
console.log('CreateChannelFromURL - Processing OAuth callback:', {
|
||||
hasClientId: !!client_id,
|
||||
hasPlatformId: !!urlPlatform,
|
||||
hasAccessToken: !!urlAccessToken,
|
||||
hasCode: !!code,
|
||||
domain: urlDomain
|
||||
});
|
||||
if (!urlDomain) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-generate channel name from domain
|
||||
const domainWithoutProtocol = decodeURIComponent(urlDomain).replace(/^https?:\/\//, '');
|
||||
const cleanName = domainWithoutProtocol.split('.')[0].replace(/[-_]/g, ' ');
|
||||
const capitalizedName = cleanName.charAt(0).toUpperCase() + cleanName.slice(1) + ' Channel';
|
||||
// Fix OpenFront capitalization specifically
|
||||
const fixedName = cleanName.replace(/openfront/gi, 'Openfront');
|
||||
const capitalizedName = fixedName.charAt(0).toUpperCase() + fixedName.slice(1) + ' Channel';
|
||||
setName(capitalizedName);
|
||||
setDomain(decodeURIComponent(urlDomain));
|
||||
|
||||
// Check if this is marketplace flow (has client_id) or long flow (has platform)
|
||||
if (client_id && client_secret && app_name && adapter_slug && urlAccessToken) {
|
||||
// Marketplace flow - OAuth already exchanged, ready to create platform/shop
|
||||
console.log('✅ Marketplace flow detected');
|
||||
if (client_id && client_secret && app_name && adapter_slug && urlAccessToken && urlAccessToken !== 'undefined') {
|
||||
// Marketplace flow - OAuth already exchanged, ready to create platform/channel
|
||||
|
||||
setIsMarketplaceFlow(true);
|
||||
setClientId(client_id);
|
||||
setClientSecret(client_secret);
|
||||
setAppName(app_name);
|
||||
setAdapterSlug(adapter_slug);
|
||||
setAccessToken(urlAccessToken);
|
||||
|
||||
if (urlRefreshToken) {
|
||||
setRefreshToken(urlRefreshToken);
|
||||
}
|
||||
if (urlTokenExpiresAt) {
|
||||
setTokenExpiresAt(urlTokenExpiresAt);
|
||||
}
|
||||
} else if (urlPlatform && urlAccessToken) {
|
||||
// Long flow - platform exists, just need to create channel
|
||||
console.log('✅ Long flow detected');
|
||||
|
||||
setPlatformId(urlPlatform);
|
||||
setAccessToken(urlAccessToken);
|
||||
|
||||
// Set additional token data if provided
|
||||
if (urlRefreshToken) setRefreshToken(urlRefreshToken);
|
||||
if (urlTokenExpiresAt) setTokenExpiresAt(urlTokenExpiresAt);
|
||||
if (urlRefreshToken) {
|
||||
setRefreshToken(urlRefreshToken);
|
||||
}
|
||||
if (urlTokenExpiresAt) {
|
||||
setTokenExpiresAt(urlTokenExpiresAt);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Invalid OAuth callback - missing required parameters');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDialogOpen(true);
|
||||
|
||||
// Clean URL params
|
||||
const newUrl = new URL(window.location.href);
|
||||
['showCreateChannel', 'platform', 'accessToken', 'domain', 'client_id', 'client_secret', 'app_name', 'adapter_slug', 'refreshToken', 'tokenExpiresAt', 'code'].forEach(param => {
|
||||
newUrl.searchParams.delete(param);
|
||||
});
|
||||
router.replace(newUrl.pathname + newUrl.search);
|
||||
// DON'T clean URL params immediately - wait for user action
|
||||
// This was causing the dialog to lose its data source
|
||||
}, [searchParams, router]);
|
||||
|
||||
const handleChannelCreation = async () => {
|
||||
@@ -143,20 +149,16 @@ export function CreateChannelFromURL({ onChannelCreated, searchParams }: CreateC
|
||||
|
||||
// Marketplace flow: Check if platform exists
|
||||
if (isMarketplaceFlow && clientId && clientSecret) {
|
||||
console.log('🔍 Checking if platform exists for client_id:', clientId);
|
||||
|
||||
const { getChannelPlatformByClientId } = await import('../actions/getChannelPlatformByClientId');
|
||||
const platformResult = await getChannelPlatformByClientId(clientId);
|
||||
|
||||
console.log('🔍 Platform check result:', platformResult);
|
||||
|
||||
if (platformResult.success && platformResult.data) {
|
||||
// Platform exists, use it
|
||||
finalPlatformId = platformResult.data.id;
|
||||
console.log('✅ Found existing platform ID:', finalPlatformId, 'Name:', platformResult.data.name);
|
||||
} else {
|
||||
// Platform doesn't exist, will create inline with shop
|
||||
console.log('📦 Platform does not exist, will create inline');
|
||||
finalPlatformId = null;
|
||||
}
|
||||
}
|
||||
@@ -168,27 +170,27 @@ export function CreateChannelFromURL({ onChannelCreated, searchParams }: CreateC
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the shop
|
||||
// Create the channel
|
||||
const channelData: any = {
|
||||
name: name.trim(),
|
||||
domain: domain.trim(),
|
||||
accessToken: finalAccessToken.trim(),
|
||||
...(refreshToken && { refreshToken: refreshToken }),
|
||||
...(tokenExpiresAt && { tokenExpiresAt: new Date(tokenExpiresAt) }),
|
||||
};
|
||||
|
||||
console.log('🏪 Creating channel with data:');
|
||||
console.log(' - finalPlatformId:', finalPlatformId);
|
||||
console.log(' - isMarketplaceFlow:', isMarketplaceFlow);
|
||||
console.log(' - clientId:', clientId);
|
||||
console.log(' - adapterSlug:', adapterSlug);
|
||||
|
||||
if (refreshToken) {
|
||||
channelData.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
if (tokenExpiresAt) {
|
||||
channelData.tokenExpiresAt = new Date(tokenExpiresAt);
|
||||
}
|
||||
|
||||
|
||||
if (finalPlatformId) {
|
||||
console.log('🔗 Using existing platform ID:', finalPlatformId);
|
||||
channelData.platformId = finalPlatformId;
|
||||
} else if (isMarketplaceFlow && clientId && clientSecret && appName && adapterSlug) {
|
||||
// Create platform inline using dynamic adapter slug
|
||||
console.log('📦 Creating platform inline with adapter:', adapterSlug);
|
||||
channelData.platform = {
|
||||
create: {
|
||||
name: appName + ' (Auto-created)',
|
||||
@@ -207,19 +209,22 @@ export function CreateChannelFromURL({ onChannelCreated, searchParams }: CreateC
|
||||
deleteWebhookFunction: adapterSlug,
|
||||
}
|
||||
};
|
||||
console.log('📦 Platform inline creation data:', channelData.platform.create);
|
||||
} else {
|
||||
console.log('❌ No platform connection - this will cause an error');
|
||||
console.log('❌ Missing: finalPlatformId?', !finalPlatformId, 'isMarketplaceFlow?', !isMarketplaceFlow, 'clientId?', !clientId, 'adapterSlug?', !adapterSlug);
|
||||
}
|
||||
|
||||
console.log('🏪 Final channelData before createChannel call:', JSON.stringify(channelData, null, 2));
|
||||
|
||||
const channelResult = await createChannel(channelData);
|
||||
|
||||
if (channelResult.success) {
|
||||
toast.success('Channel connected successfully!');
|
||||
setIsDialogOpen(false);
|
||||
|
||||
// Clean URL params after successful creation
|
||||
const newUrl = new URL(window.location.href);
|
||||
['showCreateChannel', 'platform', 'accessToken', 'domain', 'client_id', 'client_secret', 'app_name', 'adapter_slug', 'refreshToken', 'tokenExpiresAt', 'code'].forEach(param => {
|
||||
newUrl.searchParams.delete(param);
|
||||
});
|
||||
router.replace(newUrl.pathname + newUrl.search);
|
||||
|
||||
router.refresh();
|
||||
|
||||
if (onChannelCreated) {
|
||||
@@ -229,7 +234,6 @@ export function CreateChannelFromURL({ onChannelCreated, searchParams }: CreateC
|
||||
toast.error(channelResult.error || 'Failed to create channel');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating channel:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to create channel');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -255,10 +259,10 @@ export function CreateChannelFromURL({ onChannelCreated, searchParams }: CreateC
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connect OpenFront Channel</DialogTitle>
|
||||
<DialogTitle>Connect Openfront Channel</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isMarketplaceFlow
|
||||
? 'Setting up your OpenFront integration. We\'ll create the platform if needed and connect your channel.'
|
||||
? 'Setting up your Openfront integration. We\'ll create the platform if needed and connect your channel.'
|
||||
: 'Complete your channel setup with the OAuth credentials received.'
|
||||
}
|
||||
</DialogDescription>
|
||||
@@ -304,7 +308,7 @@ export function CreateChannelFromURL({ onChannelCreated, searchParams }: CreateC
|
||||
{isMarketplaceFlow && (
|
||||
<div className="text-sm bg-zinc-50 dark:bg-zinc-950/20 p-3 rounded-lg border border-zinc-200 dark:border-zinc-800">
|
||||
<p className="font-medium text-zinc-900 dark:text-zinc-100 mb-1">
|
||||
OpenFront Integration
|
||||
Openfront Integration
|
||||
</p>
|
||||
<p className="text-zinc-700 dark:text-zinc-300 text-xs">
|
||||
This platform will be created with the channel.
|
||||
|
||||
@@ -131,12 +131,9 @@ export function PlatformTabs({
|
||||
>
|
||||
<div className="text-sm font-medium leading-5 whitespace-nowrap flex items-center justify-center h-full gap-2">
|
||||
<span className="uppercase tracking-wide">{platform.name}</span>
|
||||
<Badge
|
||||
color="blue"
|
||||
className="px-1.5 py-0 text-[10px] leading-[14px] rounded-sm shadow-xs inline-flex items-center h-[18px]"
|
||||
>
|
||||
<span className="rounded-sm bg-background border shadow-xs px-1.5 py-0 text-[10px] leading-[14px] font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 inline-flex items-center h-[18px]">
|
||||
{platform.count || 0}
|
||||
</Badge>
|
||||
</span>
|
||||
{(renderEditButton || onEditPlatform) && (
|
||||
renderEditButton ? renderEditButton(platform) : (
|
||||
<Button
|
||||
|
||||
@@ -85,7 +85,7 @@ export function StatusTabs({ statusCounts, statusConfig, entityName, onSelectAll
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute bottom-[-1px] h-[2px] bg-foreground transition-all duration-300 ease-out ml-4 md:ml-6"
|
||||
className="absolute bottom-[-1px] h-[2px] bg-blue-500 transition-all duration-300 ease-out ml-4 md:ml-6"
|
||||
style={{
|
||||
left: `${activeTabOffsetLeft - scrollOffset}px`,
|
||||
width: `${activeTabWidth}px`,
|
||||
@@ -137,9 +137,9 @@ export function StatusTabs({ statusCounts, statusConfig, entityName, onSelectAll
|
||||
>
|
||||
<div className="text-sm font-medium leading-5 whitespace-nowrap flex items-center justify-center h-full gap-2">
|
||||
{status.label}
|
||||
<Badge color={config.color as any} className="px-1.5 py-0 text-[10px] leading-[14px] rounded-sm shadow-xs inline-flex items-center h-[18px]">
|
||||
<span className="rounded-sm bg-background border shadow-xs px-1.5 py-0 text-[10px] leading-[14px] font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 inline-flex items-center h-[18px]">
|
||||
{status.count}
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||
import { useRef, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Square, Store, Tv } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
@@ -144,11 +143,9 @@ export function MatchesTabs({ statusCounts, onSelectAll, selectedItems, totalIte
|
||||
>
|
||||
<div className="text-sm font-medium leading-5 whitespace-nowrap flex items-center justify-center h-full gap-2">
|
||||
{status.label}
|
||||
{status.value === "MATCHES" ? (
|
||||
<Badge color={statusConfig[status.value as keyof typeof statusConfig].color} className="px-1.5 py-0 text-[10px] leading-[14px] rounded-sm shadow-xs inline-flex items-center h-[18px]">
|
||||
{status.count}
|
||||
</Badge>
|
||||
) : null}
|
||||
<span className="rounded-sm bg-background border shadow-xs px-1.5 py-0 text-[10px] leading-[14px] font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 inline-flex items-center h-[18px]">
|
||||
{status.count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,17 +9,18 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
const statusConfig = {
|
||||
SHOP: {
|
||||
label: "Shop Products",
|
||||
icon: Layers3,
|
||||
// No icon for platforms
|
||||
color: "blue"
|
||||
},
|
||||
CHANNEL: {
|
||||
label: "Channel Products",
|
||||
icon: Database,
|
||||
// No icon for platforms
|
||||
color: "green"
|
||||
},
|
||||
MATCHES: {
|
||||
label: "Matches",
|
||||
icon: Square,
|
||||
iconColor: "text-purple-500",
|
||||
color: "purple"
|
||||
},
|
||||
} as const;
|
||||
@@ -90,13 +91,14 @@ export function StatusTabs({ statusCounts, onSelectAll, selectedItems, totalItem
|
||||
updateScroll(n => n + 1);
|
||||
};
|
||||
|
||||
const activeIndex = statuses.findIndex((s) => s.value === currentStatus);
|
||||
const activeIndex = currentStatus === "all" ? 0 : statuses.findIndex((s) => s.value === currentStatus) + 1;
|
||||
const activeTabOffsetLeft = tabRefs.current[activeIndex]?.offsetLeft || 0;
|
||||
const activeTabWidth = tabRefs.current[activeIndex]?.offsetWidth || 0;
|
||||
const scrollOffset = scrollContainerRef.current ? scrollContainerRef.current.scrollLeft : 0;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Hover background */}
|
||||
<div
|
||||
className="absolute h-[28px] mt-1 transition-all duration-300 ease-out bg-muted/60 rounded-[6px] flex items-center ml-4 md:ml-6"
|
||||
style={{
|
||||
@@ -106,11 +108,13 @@ export function StatusTabs({ statusCounts, onSelectAll, selectedItems, totalItem
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Active indicator line */}
|
||||
<div
|
||||
className="absolute bottom-[-1px] h-[2px] bg-foreground transition-all duration-300 ease-out ml-4 md:ml-6"
|
||||
className="absolute h-[2px] bg-blue-500 transition-all duration-300 ease-out ml-4 md:ml-6"
|
||||
style={{
|
||||
left: `${activeTabOffsetLeft - scrollOffset}px`,
|
||||
width: `${activeTabWidth}px`,
|
||||
bottom: '-1.5px',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -124,25 +128,32 @@ export function StatusTabs({ statusCounts, onSelectAll, selectedItems, totalItem
|
||||
/>
|
||||
)}
|
||||
{statuses.map((status, index) => {
|
||||
const StatusIcon = statusConfig[status.value as keyof typeof statusConfig].icon;
|
||||
const config = statusConfig[status.value as keyof typeof statusConfig];
|
||||
const StatusIcon = config.icon;
|
||||
const iconColor = config.iconColor;
|
||||
const isActive = currentStatus === status.value;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={status.value}
|
||||
ref={el => { tabRefs.current[index] = el }}
|
||||
ref={el => { tabRefs.current[index + 1] = el }}
|
||||
className={`px-3 py-2 cursor-pointer transition-colors duration-300 ${
|
||||
currentStatus === status.value
|
||||
isActive
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseEnter={() => setHoveredIndex(index + 1)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
onClick={() => handleStatusChange(status.value)}
|
||||
>
|
||||
<div className="text-sm font-medium leading-5 whitespace-nowrap flex items-center justify-center h-full gap-2">
|
||||
{/* Only show icon for MATCHES */}
|
||||
{StatusIcon && <StatusIcon className={`h-4 w-4 ${iconColor}`} />}
|
||||
{status.label}
|
||||
<Badge color={statusConfig[status.value as keyof typeof statusConfig].color} className="px-1.5 py-0 text-[10px] leading-[14px] rounded-sm shadow-xs inline-flex items-center h-[18px]">
|
||||
{/* Use neutral badge for all tabs */}
|
||||
<span className="rounded-sm bg-background border shadow-xs px-1.5 py-0 text-[10px] leading-[14px] font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 inline-flex items-center h-[18px]">
|
||||
{status.count}
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -164,7 +164,7 @@ export const ChannelSearchAccordion: React.FC<ChannelSearchAccordionProps> = ({
|
||||
<div className="space-y-2 mt-2 max-h-60 overflow-y-auto">
|
||||
{searchResults.map((product) => (
|
||||
<div
|
||||
key={product.productId}
|
||||
key={`${product.productId}-${product.variantId}`}
|
||||
className="flex items-center space-x-2 p-2 bg-background rounded-md border"
|
||||
>
|
||||
<div className="w-12 h-12 flex-shrink-0 rounded-md overflow-hidden">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
|
||||
// Helper function to safely format price - handles text prices that may not be numeric
|
||||
function formatPrice(price: string | number | undefined, currency: string = '$'): string {
|
||||
@@ -78,6 +78,18 @@ import { useToast } from '@/components/ui/use-toast';
|
||||
import { Order } from "../lib/types";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { deleteItemAction } from '@/features/dashboard/actions/item-actions';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
interface OrderDetailsComponentProps {
|
||||
order: Order;
|
||||
@@ -133,6 +145,37 @@ export const OrderDetailsComponent = ({
|
||||
onAction('matchOrder', order.id);
|
||||
};
|
||||
|
||||
// Delete handler exactly like EditItemDrawer
|
||||
const handleDelete = useCallback(async () => {
|
||||
try {
|
||||
const { errors } = await deleteItemAction('Order', order.id);
|
||||
|
||||
const error = errors?.find(x => x.path === undefined || x.path?.length === 1);
|
||||
if (error) {
|
||||
toast({
|
||||
title: 'Unable to delete order',
|
||||
description: error.message,
|
||||
variant: 'destructive'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Order deleted successfully'
|
||||
});
|
||||
|
||||
// Refresh the page or trigger parent refresh
|
||||
window.location.reload();
|
||||
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: 'Unable to delete order',
|
||||
description: err.message || "An unexpected error occurred",
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
}, [order.id, toast]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
@@ -197,7 +240,8 @@ export const OrderDetailsComponent = ({
|
||||
<StatusBadge status={order.status as any} />
|
||||
{currentAction && (
|
||||
<Badge
|
||||
className="uppercase tracking-wide font-medium text-xs flex items-center gap-1.5 border py-0.5"
|
||||
color="zinc"
|
||||
className="text-[.6rem] sm:text-[.7rem] py-0 px-2 sm:px-3 tracking-wide font-medium rounded-md border h-6 uppercase flex items-center gap-1.5"
|
||||
>
|
||||
<Loader2 className="size-3.5 shrink-0 animate-spin" />
|
||||
{getLoadingText(currentAction)}
|
||||
@@ -214,54 +258,52 @@ export const OrderDetailsComponent = ({
|
||||
<MoreVertical className="stroke-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onAction("getMatch", order.id)}>
|
||||
<Square
|
||||
size={16}
|
||||
className="opacity-60 mr-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
GET MATCH
|
||||
<Square size={16} className="opacity-40" strokeWidth={2} aria-hidden="true" />
|
||||
Get Match
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAction("saveMatch", order.id)}>
|
||||
<Save
|
||||
size={16}
|
||||
className="opacity-60 mr-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
SAVE MATCH
|
||||
<Save size={16} className="opacity-40" strokeWidth={2} aria-hidden="true" />
|
||||
Save Match
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAction("placeOrder", order.id)}>
|
||||
<Ticket
|
||||
size={16}
|
||||
className="opacity-60 mr-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
PLACE ORDER
|
||||
<Ticket size={16} className="opacity-40" strokeWidth={2} aria-hidden="true" />
|
||||
Place Order
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setIsEditDrawerOpen(true)}>
|
||||
<FilePenLine
|
||||
size={16}
|
||||
className="opacity-60 mr-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
EDIT ORDER
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (confirm(`Are you sure you want to delete order ${order.orderName || order.orderId}? This action cannot be undone.`)) {
|
||||
onAction("deleteOrder", order.id);
|
||||
}
|
||||
}}
|
||||
className="text-red-600 focus:text-red-600 focus:bg-red-50 dark:focus:bg-red-950/50"
|
||||
>
|
||||
<Trash2
|
||||
size={16}
|
||||
className="opacity-60 mr-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
DELETE ORDER
|
||||
<FilePenLine size={16} className="opacity-40" strokeWidth={2} aria-hidden="true" />
|
||||
Edit Order
|
||||
</DropdownMenuItem>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="text-red-600 focus:text-red-600 focus:bg-red-50 dark:focus:bg-red-950/50"
|
||||
>
|
||||
<Trash2 size={16} className="opacity-40" strokeWidth={2} aria-hidden="true" />
|
||||
Delete Order
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete order</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete{' '}
|
||||
<strong>
|
||||
Order {order.orderName || `#${order.orderId}`}
|
||||
</strong>
|
||||
? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete}>
|
||||
Yes, delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
@@ -34,7 +34,6 @@ interface StatusBadgeProps {
|
||||
export function StatusBadge({ status }: StatusBadgeProps) {
|
||||
return (
|
||||
<Badge
|
||||
color={statusConfig[status].color}
|
||||
className="text-[.6rem] sm:text-[.7rem] py-0 px-2 sm:px-3 tracking-wide font-medium rounded-md border h-6"
|
||||
>
|
||||
{status}
|
||||
|
||||
@@ -17,12 +17,26 @@ export interface CreateShopInput {
|
||||
}
|
||||
|
||||
export async function createShop(data: CreateShopInput) {
|
||||
console.log('🏪 createShop action called with data:');
|
||||
console.log(' - name:', data.name);
|
||||
console.log(' - domain:', data.domain);
|
||||
console.log(' - accessToken present:', !!data.accessToken);
|
||||
console.log(' - refreshToken present:', !!data.refreshToken);
|
||||
console.log(' - refreshToken value:', data.refreshToken ? `${data.refreshToken.substring(0, 10)}...` : 'MISSING');
|
||||
console.log(' - tokenExpiresAt present:', !!data.tokenExpiresAt);
|
||||
console.log(' - tokenExpiresAt value:', data.tokenExpiresAt);
|
||||
console.log(' - platformId:', data.platformId);
|
||||
console.log(' - platform.create present:', !!data.platform?.create);
|
||||
|
||||
const mutation = `
|
||||
mutation CreateShop($data: ShopCreateInput!) {
|
||||
createShop(data: $data) {
|
||||
id
|
||||
name
|
||||
domain
|
||||
accessToken
|
||||
refreshToken
|
||||
tokenExpiresAt
|
||||
platform {
|
||||
id
|
||||
name
|
||||
@@ -35,31 +49,76 @@ export async function createShop(data: CreateShopInput) {
|
||||
let platformData;
|
||||
if (data.platformId) {
|
||||
// Existing platform - connect by ID
|
||||
console.log('🔗 Using existing platform ID:', data.platformId);
|
||||
platformData = { connect: { id: data.platformId } };
|
||||
} else if (data.platform?.create) {
|
||||
// Inline platform creation
|
||||
console.log('📦 Creating platform inline:', data.platform.create);
|
||||
platformData = { create: data.platform.create };
|
||||
} else {
|
||||
console.log('❌ No platform connection method provided');
|
||||
throw new Error('Either platformId or platform.create must be provided');
|
||||
}
|
||||
|
||||
const variables = {
|
||||
const variables: {
|
||||
data: {
|
||||
name: string;
|
||||
domain: string;
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
tokenExpiresAt?: string;
|
||||
platform?: any;
|
||||
}
|
||||
} = {
|
||||
data: {
|
||||
name: data.name,
|
||||
domain: data.domain,
|
||||
accessToken: data.accessToken,
|
||||
...(data.refreshToken && { refreshToken: data.refreshToken }),
|
||||
...(data.tokenExpiresAt && { tokenExpiresAt: data.tokenExpiresAt.toISOString() }),
|
||||
platform: platformData
|
||||
}
|
||||
};
|
||||
|
||||
console.log('🔍 Building GraphQL variables:');
|
||||
console.log(' - Base variables:', JSON.stringify(variables, null, 2));
|
||||
|
||||
if (data.refreshToken) {
|
||||
console.log('🔑 Adding refreshToken to variables:', data.refreshToken ? `${data.refreshToken.substring(0, 10)}...` : 'MISSING');
|
||||
variables.data.refreshToken = data.refreshToken;
|
||||
} else {
|
||||
console.log('❌ No refreshToken to add');
|
||||
}
|
||||
|
||||
if (data.tokenExpiresAt) {
|
||||
console.log('⏰ Adding tokenExpiresAt to variables:', data.tokenExpiresAt);
|
||||
variables.data.tokenExpiresAt = data.tokenExpiresAt.toISOString();
|
||||
} else {
|
||||
console.log('❌ No tokenExpiresAt to add');
|
||||
}
|
||||
|
||||
variables.data.platform = platformData;
|
||||
|
||||
console.log('🚀 Final GraphQL variables being sent:');
|
||||
console.log(JSON.stringify(variables, null, 2));
|
||||
|
||||
const response = await keystoneClient(mutation, variables);
|
||||
console.log('Created shop response:', response);
|
||||
console.log('📨 GraphQL response received:');
|
||||
console.log(' - success:', response.success);
|
||||
console.log(' - data:', JSON.stringify(response.data, null, 2));
|
||||
console.log(' - error:', response.error);
|
||||
|
||||
if (response.success && response.data?.createShop) {
|
||||
console.log('✅ Shop created successfully:');
|
||||
console.log(' - ID:', response.data.createShop.id);
|
||||
console.log(' - Name:', response.data.createShop.name);
|
||||
console.log(' - Domain:', response.data.createShop.domain);
|
||||
console.log(' - Has accessToken:', !!response.data.createShop.accessToken);
|
||||
console.log(' - Has refreshToken:', !!response.data.createShop.refreshToken);
|
||||
console.log(' - TokenExpiresAt:', response.data.createShop.tokenExpiresAt);
|
||||
return { success: true, shop: response.data.createShop };
|
||||
} else {
|
||||
console.log('❌ Shop creation failed:');
|
||||
console.log(' - Response success:', response.success);
|
||||
console.log(' - Response error:', response.error);
|
||||
console.log(' - Full response:', JSON.stringify(response, null, 2));
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || 'Failed to create shop'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { createShop } from "../actions/createShop";
|
||||
import { createShop, CreateShopInput } from "../actions/createShop";
|
||||
import { createShopPlatform } from "../actions/createShopPlatform";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -76,14 +76,6 @@ export function CreateShopFromURL({ onShopCreated, searchParams }: CreateShopFro
|
||||
|
||||
if (!urlDomain) return;
|
||||
|
||||
console.log('CreateShopFromURL - Processing OAuth callback:', {
|
||||
hasClientId: !!client_id,
|
||||
hasPlatformId: !!urlPlatform,
|
||||
hasAccessToken: !!urlAccessToken,
|
||||
hasCode: !!code,
|
||||
domain: urlDomain
|
||||
});
|
||||
|
||||
// Auto-generate shop name from domain
|
||||
const domainWithoutProtocol = decodeURIComponent(urlDomain).replace(/^https?:\/\//, '');
|
||||
const cleanName = domainWithoutProtocol.split('.')[0].replace(/[-_]/g, ' ');
|
||||
@@ -92,37 +84,38 @@ export function CreateShopFromURL({ onShopCreated, searchParams }: CreateShopFro
|
||||
setDomain(decodeURIComponent(urlDomain));
|
||||
|
||||
// Check if this is marketplace flow (has client_id) or long flow (has platform)
|
||||
if (client_id && client_secret && app_name && adapter_slug && urlAccessToken) {
|
||||
if (client_id && client_secret && app_name && adapter_slug && urlAccessToken && urlAccessToken !== 'undefined') {
|
||||
// Marketplace flow - OAuth already exchanged, ready to create platform/shop
|
||||
console.log('✅ Marketplace flow detected');
|
||||
setIsMarketplaceFlow(true);
|
||||
setClientId(client_id);
|
||||
setClientSecret(client_secret);
|
||||
setAppName(app_name);
|
||||
setAdapterSlug(adapter_slug);
|
||||
setAccessToken(urlAccessToken);
|
||||
|
||||
if (urlRefreshToken) {
|
||||
setRefreshToken(urlRefreshToken);
|
||||
}
|
||||
if (urlTokenExpiresAt) {
|
||||
setTokenExpiresAt(urlTokenExpiresAt);
|
||||
}
|
||||
} else if (urlPlatform && urlAccessToken) {
|
||||
// Long flow - platform exists, just need to create shop
|
||||
console.log('✅ Long flow detected');
|
||||
setPlatformId(urlPlatform);
|
||||
setAccessToken(urlAccessToken);
|
||||
|
||||
// Set additional token data if provided
|
||||
if (urlRefreshToken) setRefreshToken(urlRefreshToken);
|
||||
if (urlTokenExpiresAt) setTokenExpiresAt(urlTokenExpiresAt);
|
||||
if (urlRefreshToken) {
|
||||
setRefreshToken(urlRefreshToken);
|
||||
}
|
||||
if (urlTokenExpiresAt) {
|
||||
setTokenExpiresAt(urlTokenExpiresAt);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Invalid OAuth callback - missing required parameters');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDialogOpen(true);
|
||||
|
||||
// Clean URL params
|
||||
const newUrl = new URL(window.location.href);
|
||||
['showCreateShop', 'platform', 'accessToken', 'domain', 'client_id', 'client_secret', 'app_name', 'adapter_slug', 'refreshToken', 'tokenExpiresAt', 'code'].forEach(param => {
|
||||
newUrl.searchParams.delete(param);
|
||||
});
|
||||
router.replace(newUrl.pathname + newUrl.search);
|
||||
}, [searchParams, router]);
|
||||
|
||||
const handleShopCreation = async () => {
|
||||
@@ -143,20 +136,14 @@ export function CreateShopFromURL({ onShopCreated, searchParams }: CreateShopFro
|
||||
|
||||
// Marketplace flow: Check if platform exists
|
||||
if (isMarketplaceFlow && clientId && clientSecret) {
|
||||
console.log('🔍 Checking if platform exists for client_id:', clientId);
|
||||
|
||||
const { getShopPlatformByClientId } = await import('../actions/getShopPlatformByClientId');
|
||||
const platformResult = await getShopPlatformByClientId(clientId);
|
||||
|
||||
console.log('🔍 Platform check result:', platformResult);
|
||||
|
||||
if (platformResult.success && platformResult.data) {
|
||||
// Platform exists, use it
|
||||
finalPlatformId = platformResult.data.id;
|
||||
console.log('✅ Found existing platform ID:', finalPlatformId, 'Name:', platformResult.data.name);
|
||||
} else {
|
||||
// Platform doesn't exist, will create inline with shop
|
||||
console.log('📦 Platform does not exist, will create inline');
|
||||
finalPlatformId = null;
|
||||
}
|
||||
}
|
||||
@@ -169,26 +156,24 @@ export function CreateShopFromURL({ onShopCreated, searchParams }: CreateShopFro
|
||||
}
|
||||
|
||||
// Create the shop
|
||||
const shopData: any = {
|
||||
const shopData: CreateShopInput = {
|
||||
name: name.trim(),
|
||||
domain: domain.trim(),
|
||||
accessToken: finalAccessToken.trim(),
|
||||
...(refreshToken && { refreshToken: refreshToken }),
|
||||
...(tokenExpiresAt && { tokenExpiresAt: new Date(tokenExpiresAt) }),
|
||||
};
|
||||
|
||||
console.log('🏪 Creating shop with data:');
|
||||
console.log(' - finalPlatformId:', finalPlatformId);
|
||||
console.log(' - isMarketplaceFlow:', isMarketplaceFlow);
|
||||
console.log(' - clientId:', clientId);
|
||||
console.log(' - adapterSlug:', adapterSlug);
|
||||
if (refreshToken) {
|
||||
shopData.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
if (tokenExpiresAt) {
|
||||
shopData.tokenExpiresAt = new Date(tokenExpiresAt);
|
||||
}
|
||||
|
||||
if (finalPlatformId) {
|
||||
console.log('🔗 Using existing platform ID:', finalPlatformId);
|
||||
shopData.platformId = finalPlatformId;
|
||||
} else if (isMarketplaceFlow && clientId && clientSecret && appName && adapterSlug) {
|
||||
// Create platform inline using dynamic adapter slug
|
||||
console.log('📦 Creating platform inline with adapter:', adapterSlug);
|
||||
shopData.platform = {
|
||||
create: {
|
||||
name: appName + ' (Auto-created)',
|
||||
@@ -211,19 +196,21 @@ export function CreateShopFromURL({ onShopCreated, searchParams }: CreateShopFro
|
||||
deleteWebhookFunction: adapterSlug,
|
||||
}
|
||||
};
|
||||
console.log('📦 Platform inline creation data:', shopData.platform.create);
|
||||
} else {
|
||||
console.log('❌ No platform connection - this will cause an error');
|
||||
console.log('❌ Missing: finalPlatformId?', !finalPlatformId, 'isMarketplaceFlow?', !isMarketplaceFlow, 'clientId?', !clientId, 'adapterSlug?', !adapterSlug);
|
||||
}
|
||||
|
||||
console.log('🏪 Final shopData before createShop call:', JSON.stringify(shopData, null, 2));
|
||||
|
||||
const shopResult = await createShop(shopData);
|
||||
|
||||
if (shopResult.success) {
|
||||
toast.success('Shop connected successfully!');
|
||||
setIsDialogOpen(false);
|
||||
|
||||
// Clean URL params after successful creation
|
||||
const newUrl = new URL(window.location.href);
|
||||
['showCreateShop', 'platform', 'accessToken', 'domain', 'client_id', 'client_secret', 'app_name', 'adapter_slug', 'refreshToken', 'tokenExpiresAt', 'code'].forEach(param => {
|
||||
newUrl.searchParams.delete(param);
|
||||
});
|
||||
router.replace(newUrl.pathname + newUrl.search);
|
||||
|
||||
router.refresh();
|
||||
|
||||
if (onShopCreated) {
|
||||
@@ -259,10 +246,10 @@ export function CreateShopFromURL({ onShopCreated, searchParams }: CreateShopFro
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connect OpenFront Store</DialogTitle>
|
||||
<DialogTitle>Connect Openfront Store</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isMarketplaceFlow
|
||||
? 'Setting up your OpenFront integration. We\'ll create the platform if needed and connect your store.'
|
||||
? 'Setting up your Openfront integration. We\'ll create the platform if needed and connect your store.'
|
||||
: 'Complete your shop setup with the OAuth credentials received.'
|
||||
}
|
||||
</DialogDescription>
|
||||
@@ -308,7 +295,7 @@ export function CreateShopFromURL({ onShopCreated, searchParams }: CreateShopFro
|
||||
{isMarketplaceFlow && (
|
||||
<div className="text-sm bg-zinc-50 dark:bg-zinc-950/20 p-3 rounded-lg border border-zinc-200 dark:border-zinc-800">
|
||||
<p className="font-medium text-zinc-900 dark:text-zinc-100 mb-1">
|
||||
openfront Integration
|
||||
Openfront Integration
|
||||
</p>
|
||||
<p className="text-zinc-700 dark:text-zinc-300 text-xs">
|
||||
This platform will be created with the shop.
|
||||
|
||||
Reference in New Issue
Block a user