Skip to content

Commit 16308e6

Browse files
Implement Shopify App
Replace simulation with a real Shopify app using Firebase Functions.
1 parent a963d19 commit 16308e6

File tree

9 files changed

+809
-26
lines changed

9 files changed

+809
-26
lines changed

api/shopify-auth-callback.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Proxy pour le callback OAuth Shopify
2+
export default async function handler(req, res) {
3+
if (req.method !== 'POST') {
4+
return res.status(405).json({ error: 'Method not allowed' });
5+
}
6+
7+
try {
8+
// Forward la requête vers Firebase Functions
9+
const firebaseUrl = 'https://us-central1-refspring-project.cloudfunctions.net/shopifyTokenExchange';
10+
11+
const response = await fetch(firebaseUrl, {
12+
method: 'POST',
13+
headers: {
14+
'Content-Type': 'application/json',
15+
},
16+
body: JSON.stringify(req.body)
17+
});
18+
19+
const data = await response.json();
20+
21+
if (!response.ok) {
22+
return res.status(response.status).json(data);
23+
}
24+
25+
res.json(data);
26+
} catch (error) {
27+
console.error('OAuth callback proxy error:', error);
28+
res.status(500).json({ error: 'Internal server error' });
29+
}
30+
}

api/shopify-config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default async function handler(req, res) {
66

77
try {
88
// Forward la requête vers Firebase Functions
9-
const firebaseUrl = 'https://us-central1-refspring-project.cloudfunctions.net/shopifyInstall';
9+
const firebaseUrl = 'https://us-central1-refspring-project.cloudfunctions.net/shopifyAuthUrl';
1010

1111
const response = await fetch(firebaseUrl, {
1212
method: 'POST',

api/shopify-webhooks.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Proxy pour les webhooks Shopify
2+
export default async function handler(req, res) {
3+
if (req.method !== 'POST') {
4+
return res.status(405).json({ error: 'Method not allowed' });
5+
}
6+
7+
try {
8+
const topic = req.headers['x-shopify-topic'];
9+
let firebaseUrl;
10+
11+
// Router vers la bonne fonction selon le type de webhook
12+
switch (topic) {
13+
case 'orders/create':
14+
case 'orders/updated':
15+
case 'orders/paid':
16+
firebaseUrl = 'https://us-central1-refspring-project.cloudfunctions.net/shopifyOrderWebhook';
17+
break;
18+
case 'app/uninstalled':
19+
firebaseUrl = 'https://us-central1-refspring-project.cloudfunctions.net/shopifyAppWebhook';
20+
break;
21+
default:
22+
return res.status(400).json({ error: 'Unsupported webhook topic' });
23+
}
24+
25+
// Forward la requête avec tous les headers Shopify
26+
const response = await fetch(firebaseUrl, {
27+
method: 'POST',
28+
headers: {
29+
'Content-Type': 'application/json',
30+
'X-Shopify-Topic': req.headers['x-shopify-topic'] || '',
31+
'X-Shopify-Hmac-Sha256': req.headers['x-shopify-hmac-sha256'] || '',
32+
'X-Shopify-Shop-Domain': req.headers['x-shopify-shop-domain'] || '',
33+
},
34+
body: JSON.stringify(req.body)
35+
});
36+
37+
const data = await response.json();
38+
39+
if (!response.ok) {
40+
return res.status(response.status).json(data);
41+
}
42+
43+
res.json(data);
44+
} catch (error) {
45+
console.error('Webhook proxy error:', error);
46+
res.status(500).json({ error: 'Internal server error' });
47+
}
48+
}

functions/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { validateTracking } from './validateTracking';
66
import { calculateCommissions } from './calculateCommissions';
77
import { antifraudCheck } from './antifraudCheck';
88
import { wordpressConfig, shopifyInstall, generatePluginApiKey } from './pluginAPI';
9+
import { shopifyAuthUrl, shopifyTokenExchange } from './shopifyOAuth';
10+
import { shopifyOrderWebhook, shopifyAppWebhook } from './shopifyWebhooks';
11+
import { setupShopifyWebhooks, checkShopifyInstallation } from './shopifySetup';
912
import { stripeGetPaymentMethods } from './stripeGetPaymentMethods';
1013
import { stripeDeletePaymentMethod } from './stripeDeletePaymentMethod';
1114
import { stripeSetDefaultPaymentMethod } from './stripeSetDefaultPaymentMethod';
@@ -27,6 +30,12 @@ export {
2730
wordpressConfig,
2831
shopifyInstall,
2932
generatePluginApiKey,
33+
shopifyAuthUrl,
34+
shopifyTokenExchange,
35+
shopifyOrderWebhook,
36+
shopifyAppWebhook,
37+
setupShopifyWebhooks,
38+
checkShopifyInstallation,
3039
stripeGetPaymentMethods,
3140
stripeDeletePaymentMethod,
3241
stripeSetDefaultPaymentMethod,

functions/src/pluginAPI.ts

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const wordpressConfig = functions.https.onRequest((request, response) =>
7979
});
8080
});
8181

82-
// API pour l'installation Shopify
82+
// API pour l'installation Shopify (redirige vers le processus OAuth)
8383
export const shopifyInstall = functions.https.onRequest((request, response) => {
8484
corsHandler(request, response, async () => {
8585
if (request.method !== 'POST') {
@@ -90,35 +90,20 @@ export const shopifyInstall = functions.https.onRequest((request, response) => {
9090
const installData: ShopifyInstall = request.body;
9191

9292
// Validation
93-
if (!installData.shop || !installData.code || !installData.campaignId) {
93+
if (!installData.shop || !installData.campaignId) {
9494
return response.status(400).json({ error: 'Missing required fields' });
9595
}
9696

97-
// Échanger le code OAuth contre un access token (simulation)
98-
// En réalité, on ferait un appel à l'API Shopify
99-
const accessToken = 'simulated_access_token_' + Date.now();
100-
101-
// Stocker la configuration Shopify
102-
const shopifyDoc = await admin.firestore()
103-
.collection('plugin_configs')
104-
.add({
105-
pluginType: 'shopify',
106-
domain: installData.shop,
107-
campaignId: installData.campaignId,
108-
accessToken,
109-
createdAt: new Date(),
110-
updatedAt: new Date(),
111-
active: true,
112-
settings: {
113-
autoInject: true,
114-
trackingEnabled: true
115-
}
116-
});
117-
97+
// Génération de l'état OAuth sécurisé
98+
const state = Buffer.from(`${installData.campaignId}:${Date.now()}`).toString('base64');
99+
100+
// Rediriger vers le processus OAuth réel
118101
response.json({
119102
success: true,
120-
pluginId: shopifyDoc.id,
121-
message: 'Shopify app installed successfully'
103+
requiresOAuth: true,
104+
state,
105+
message: 'Please complete OAuth authorization',
106+
nextStep: 'oauth'
122107
});
123108

124109
} catch (error) {

functions/src/shopifyApp.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as functions from 'firebase-functions';
2+
3+
// Configuration de l'app Shopify
4+
export interface ShopifyAppConfig {
5+
apiKey: string;
6+
apiSecret: string;
7+
scopes: string[];
8+
appUrl: string;
9+
webhookSecret: string;
10+
}
11+
12+
// Récupérer la configuration de l'app Shopify
13+
export function shopifyAppConfig(): ShopifyAppConfig {
14+
// En production, ces valeurs doivent être dans les secrets Firebase
15+
// Pour le développement, on peut utiliser des variables d'environnement
16+
const config = functions.config();
17+
18+
return {
19+
apiKey: config.shopify?.api_key || process.env.SHOPIFY_API_KEY || '',
20+
apiSecret: config.shopify?.api_secret || process.env.SHOPIFY_API_SECRET || '',
21+
scopes: ['read_orders', 'read_products', 'write_script_tags', 'read_customers'],
22+
appUrl: config.app?.url || process.env.APP_URL || 'https://refspring.com',
23+
webhookSecret: config.shopify?.webhook_secret || process.env.SHOPIFY_WEBHOOK_SECRET || ''
24+
};
25+
}
26+
27+
// Valider la configuration de l'app
28+
export function validateShopifyConfig(): boolean {
29+
const config = shopifyAppConfig();
30+
31+
if (!config.apiKey || !config.apiSecret) {
32+
console.error('Shopify API credentials not configured');
33+
return false;
34+
}
35+
36+
return true;
37+
}
38+
39+
// Générer un state sécurisé pour OAuth
40+
export function generateOAuthState(campaignId: string, userId: string): string {
41+
const timestamp = Date.now().toString();
42+
const data = `${campaignId}:${userId}:${timestamp}`;
43+
return Buffer.from(data).toString('base64');
44+
}
45+
46+
// Valider et décoder le state OAuth
47+
export function validateOAuthState(state: string): { campaignId: string; userId: string; timestamp: number } | null {
48+
try {
49+
const decoded = Buffer.from(state, 'base64').toString();
50+
const parts = decoded.split(':');
51+
52+
if (parts.length !== 3) {
53+
return null;
54+
}
55+
56+
const [campaignId, userId, timestampStr] = parts;
57+
const timestamp = parseInt(timestampStr);
58+
59+
// Vérifier que le state n'est pas trop ancien (30 minutes max)
60+
const maxAge = 30 * 60 * 1000; // 30 minutes
61+
if (Date.now() - timestamp > maxAge) {
62+
console.error('OAuth state expired');
63+
return null;
64+
}
65+
66+
return { campaignId, userId, timestamp };
67+
} catch (error) {
68+
console.error('Invalid OAuth state:', error);
69+
return null;
70+
}
71+
}
72+
73+
// Types pour les objets Shopify
74+
export interface ShopifyOrder {
75+
id: number;
76+
total_price: string;
77+
currency: string;
78+
customer: {
79+
id: number;
80+
email: string;
81+
};
82+
line_items: Array<{
83+
id: number;
84+
title: string;
85+
quantity: number;
86+
price: string;
87+
}>;
88+
created_at: string;
89+
}
90+
91+
export interface ShopifyWebhookData {
92+
shop_domain: string;
93+
order?: ShopifyOrder;
94+
[key: string]: any;
95+
}

0 commit comments

Comments
 (0)