|
@@ -165,6 +165,7 @@ serve(wrapHandler('oauth-shopify', async (req) => {
|
|
|
// ========================================================================
|
|
// ========================================================================
|
|
|
if (action === 'init') {
|
|
if (action === 'init') {
|
|
|
const shop = url.searchParams.get('shop')
|
|
const shop = url.searchParams.get('shop')
|
|
|
|
|
+ const phoneNumberId = url.searchParams.get('phoneNumberId')
|
|
|
|
|
|
|
|
if (!shop) {
|
|
if (!shop) {
|
|
|
return new Response(
|
|
return new Response(
|
|
@@ -173,6 +174,13 @@ serve(wrapHandler('oauth-shopify', async (req) => {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (!phoneNumberId) {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({ error: 'phoneNumberId parameter is required' }),
|
|
|
|
|
+ { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Validate shop domain
|
|
// Validate shop domain
|
|
|
const validation = validateShopDomain(shop)
|
|
const validation = validateShopDomain(shop)
|
|
|
if (!validation.valid) {
|
|
if (!validation.valid) {
|
|
@@ -202,10 +210,33 @@ serve(wrapHandler('oauth-shopify', async (req) => {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Verify phone number is available and belongs to user
|
|
|
|
|
+ const { data: phoneNumber, error: phoneError } = await supabase
|
|
|
|
|
+ .from('phone_numbers')
|
|
|
|
|
+ .select('id, is_available')
|
|
|
|
|
+ .eq('id', phoneNumberId)
|
|
|
|
|
+ .single()
|
|
|
|
|
+
|
|
|
|
|
+ if (phoneError || !phoneNumber) {
|
|
|
|
|
+ console.error('[Shopify] Phone number not found:', phoneError)
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({ error: 'Invalid phone number selected' }),
|
|
|
|
|
+ { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!phoneNumber.is_available) {
|
|
|
|
|
+ console.error('[Shopify] Phone number not available:', phoneNumberId)
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({ error: 'Selected phone number is no longer available' }),
|
|
|
|
|
+ { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Generate state parameter for CSRF protection
|
|
// Generate state parameter for CSRF protection
|
|
|
const state = crypto.randomUUID()
|
|
const state = crypto.randomUUID()
|
|
|
|
|
|
|
|
- // Store state in database
|
|
|
|
|
|
|
+ // Store state in database with phone_number_id
|
|
|
const { error: stateError } = await supabase
|
|
const { error: stateError } = await supabase
|
|
|
.from('oauth_states')
|
|
.from('oauth_states')
|
|
|
.insert({
|
|
.insert({
|
|
@@ -213,6 +244,7 @@ serve(wrapHandler('oauth-shopify', async (req) => {
|
|
|
user_id: user.id,
|
|
user_id: user.id,
|
|
|
platform: 'shopify',
|
|
platform: 'shopify',
|
|
|
shopname: validation.normalized,
|
|
shopname: validation.normalized,
|
|
|
|
|
+ phone_number_id: phoneNumberId,
|
|
|
expires_at: new Date(Date.now() + 15 * 60 * 1000).toISOString() // 15 minutes
|
|
expires_at: new Date(Date.now() + 15 * 60 * 1000).toISOString() // 15 minutes
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -377,7 +409,39 @@ serve(wrapHandler('oauth-shopify', async (req) => {
|
|
|
const storeName = shopData?.name || validation.normalized?.split('.')[0] || 'Unknown Store'
|
|
const storeName = shopData?.name || validation.normalized?.split('.')[0] || 'Unknown Store'
|
|
|
const shopDomain = validation.normalized!
|
|
const shopDomain = validation.normalized!
|
|
|
|
|
|
|
|
- // Store credentials in database
|
|
|
|
|
|
|
+ // Verify phone number is still available
|
|
|
|
|
+ const phoneNumberId = stateData.phone_number_id
|
|
|
|
|
+ if (!phoneNumberId) {
|
|
|
|
|
+ console.error('[Shopify] No phone number ID in state')
|
|
|
|
|
+ await supabaseAdmin.from('oauth_states').delete().eq('state', state)
|
|
|
|
|
+ return new Response(null, {
|
|
|
|
|
+ status: 302,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Location': `${frontendUrl}/webshops?error=phone_number_missing`
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { data: phoneNumber, error: phoneError } = await supabaseAdmin
|
|
|
|
|
+ .from('phone_numbers')
|
|
|
|
|
+ .select('id, is_available')
|
|
|
|
|
+ .eq('id', phoneNumberId)
|
|
|
|
|
+ .single()
|
|
|
|
|
+
|
|
|
|
|
+ if (phoneError || !phoneNumber || !phoneNumber.is_available) {
|
|
|
|
|
+ console.error('[Shopify] Phone number no longer available:', phoneNumberId)
|
|
|
|
|
+ await supabaseAdmin.from('oauth_states').delete().eq('state', state)
|
|
|
|
|
+ return new Response(null, {
|
|
|
|
|
+ status: 302,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Location': `${frontendUrl}/webshops?error=phone_number_unavailable`
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Store credentials in database with phone_number_id
|
|
|
const { data: insertedStore, error: insertError } = await supabaseAdmin
|
|
const { data: insertedStore, error: insertError } = await supabaseAdmin
|
|
|
.from('stores')
|
|
.from('stores')
|
|
|
.insert({
|
|
.insert({
|
|
@@ -387,6 +451,7 @@ serve(wrapHandler('oauth-shopify', async (req) => {
|
|
|
store_url: shopDomain,
|
|
store_url: shopDomain,
|
|
|
api_key: tokenResult.accessToken,
|
|
api_key: tokenResult.accessToken,
|
|
|
scopes: tokenResult.scopes || SHOPIFY_SCOPES,
|
|
scopes: tokenResult.scopes || SHOPIFY_SCOPES,
|
|
|
|
|
+ phone_number_id: phoneNumberId,
|
|
|
data_access_permissions: {
|
|
data_access_permissions: {
|
|
|
allow_customer_access: true,
|
|
allow_customer_access: true,
|
|
|
allow_order_access: true,
|
|
allow_order_access: true,
|
|
@@ -405,11 +470,9 @@ serve(wrapHandler('oauth-shopify', async (req) => {
|
|
|
.select('id')
|
|
.select('id')
|
|
|
.single()
|
|
.single()
|
|
|
|
|
|
|
|
- // Clean up state
|
|
|
|
|
- await supabaseAdmin.from('oauth_states').delete().eq('state', state)
|
|
|
|
|
-
|
|
|
|
|
if (insertError || !insertedStore) {
|
|
if (insertError || !insertedStore) {
|
|
|
console.error('[Shopify] Error storing credentials:', insertError)
|
|
console.error('[Shopify] Error storing credentials:', insertError)
|
|
|
|
|
+ await supabaseAdmin.from('oauth_states').delete().eq('state', state)
|
|
|
return new Response(null, {
|
|
return new Response(null, {
|
|
|
status: 302,
|
|
status: 302,
|
|
|
headers: {
|
|
headers: {
|
|
@@ -419,7 +482,34 @@ serve(wrapHandler('oauth-shopify', async (req) => {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- console.log(`[Shopify] Store connected successfully: ${storeName} (${shopDomain})`)
|
|
|
|
|
|
|
+ // Update phone number to mark as assigned (is_available=false, store_id)
|
|
|
|
|
+ const { error: phoneUpdateError } = await supabaseAdmin
|
|
|
|
|
+ .from('phone_numbers')
|
|
|
|
|
+ .update({
|
|
|
|
|
+ is_available: false,
|
|
|
|
|
+ store_id: insertedStore.id,
|
|
|
|
|
+ updated_at: new Date().toISOString()
|
|
|
|
|
+ })
|
|
|
|
|
+ .eq('id', phoneNumberId)
|
|
|
|
|
+
|
|
|
|
|
+ if (phoneUpdateError) {
|
|
|
|
|
+ console.error('[Shopify] Error updating phone number:', phoneUpdateError)
|
|
|
|
|
+ // Rollback: delete the store
|
|
|
|
|
+ await supabaseAdmin.from('stores').delete().eq('id', insertedStore.id)
|
|
|
|
|
+ await supabaseAdmin.from('oauth_states').delete().eq('state', state)
|
|
|
|
|
+ return new Response(null, {
|
|
|
|
|
+ status: 302,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Location': `${frontendUrl}/webshops?error=phone_assignment_failed`
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Clean up state
|
|
|
|
|
+ await supabaseAdmin.from('oauth_states').delete().eq('state', state)
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`[Shopify] Store connected successfully: ${storeName} (${shopDomain}) with phone number ${phoneNumberId}`)
|
|
|
|
|
|
|
|
// Trigger auto-sync in background
|
|
// Trigger auto-sync in background
|
|
|
const triggerSyncUrl = `${supabaseUrl}/functions/v1/trigger-sync`
|
|
const triggerSyncUrl = `${supabaseUrl}/functions/v1/trigger-sync`
|