Skip to content

Commit 48eb0f5

Browse files
authored
Merge pull request #309 from richzw/master
feat: support window store
2 parents c6141d7 + 18c2bcf commit 48eb0f5

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

microsoftstore/model.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package microsoftstore
2+
3+
import "time"
4+
5+
type UserIdentity struct {
6+
IdentityType string `json:"identityType"`
7+
IdentityValue string `json:"identityValue"`
8+
LocalTicketReference string `json:"localTicketReference"`
9+
}
10+
11+
type ProductSkuId struct {
12+
ProductId string `json:"productId"`
13+
SkuId string `json:"skuId"`
14+
}
15+
16+
type ProductType string
17+
18+
const (
19+
Application ProductType = "Application"
20+
Durable ProductType = "Durable"
21+
Game ProductType = "Game"
22+
UnmanagedConsumable ProductType = "UnmanagedConsumable"
23+
)
24+
25+
type IAPRequest struct {
26+
Beneficiaries []UserIdentity `json:"beneficiaries"`
27+
ContinuationToken string `json:"continuationToken,omitempty"`
28+
MaxPageSize int `json:"maxPageSize,omitempty"`
29+
ModifiedAfter *time.Time `json:"modifiedAfter,omitempty"`
30+
ParentProductId string `json:"parentProductId,omitempty"`
31+
ProductSkuIds []ProductSkuId `json:"productSkuIds,omitempty"`
32+
ProductTypes []ProductType `json:"productTypes"`
33+
ValidityType string `json:"validityType,omitempty"`
34+
}
35+
36+
type IdentityContractV6 struct {
37+
IdentityType string `json:"identityType"` // Contains the value "pub".
38+
IdentityValue string `json:"identityValue"` // The string value of the publisherUserId from the specified Microsoft Store ID key.
39+
}
40+
41+
// CollectionItemContractV6 represents an item in the user's collection.
42+
type CollectionItemContractV6 struct {
43+
AcquiredDate time.Time `json:"acquiredDate"` // The date on which the user acquired the item.
44+
CampaignId *string `json:"campaignId,omitempty"` // The campaign ID that was provided at purchase time for this item.
45+
DevOfferId *string `json:"devOfferId,omitempty"` // The offer ID from an in-app purchase.
46+
EndDate time.Time `json:"endDate"` // The end date of the item.
47+
FulfillmentData []string `json:"fulfillmentData,omitempty"` // N/A
48+
InAppOfferToken *string `json:"inAppOfferToken,omitempty"` // The developer-specified product ID string assigned to the item in Partner Center.
49+
ItemId string `json:"itemId"` // An ID that identifies this collection item from other items the user owns.
50+
LocalTicketReference string `json:"localTicketReference"` // The ID of the previously supplied localTicketReference in the request body.
51+
ModifiedDate time.Time `json:"modifiedDate"` // The date this item was last modified.
52+
OrderId *string `json:"orderId,omitempty"` // If present, the order ID of which this item was obtained.
53+
OrderLineItemId *string `json:"orderLineItemId,omitempty"` // If present, the line item of the particular order for which this item was obtained.
54+
OwnershipType string `json:"ownershipType"` // The string "OwnedByBeneficiary".
55+
ProductId string `json:"productId"` // The Store ID for the product in the Microsoft Store catalog.
56+
ProductType string `json:"productType"` // One of the following product types: Application, Durable, UnmanagedConsumable.
57+
PurchasedCountry *string `json:"purchasedCountry,omitempty"` // N/A
58+
Purchaser *IdentityContractV6 `json:"purchaser,omitempty"` // Represents the identity of the purchaser of the item.
59+
Quantity *int `json:"quantity,omitempty"` // The quantity of the item. Currently, this will always be 1.
60+
SkuId string `json:"skuId"` // The Store ID for the product's SKU in the Microsoft Store catalog.
61+
SkuType string `json:"skuType"` // Type of the SKU. Possible values include Trial, Full, and Rental.
62+
StartDate time.Time `json:"startDate"` // The date that the item starts being valid.
63+
Status string `json:"status"` // The status of the item. Possible values include Active, Expired, Revoked, and Banned.
64+
Tags []string `json:"tags"` // N/A
65+
TransactionId string `json:"transactionId"` // The transaction ID as a result of the purchase of this item.
66+
}
67+
68+
type IAPResponse struct {
69+
ContinuationToken *string `json:"continuationToken,omitempty"` // Token to retrieve remaining products if there are multiple sets.
70+
Items []CollectionItemContractV6 `json:"items,omitempty"` // An array of products for the specified user.
71+
}

microsoftstore/validator.go

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package microsoftstore
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
"time"
12+
)
13+
14+
const (
15+
resource = "https://onestore.microsoft.com"
16+
)
17+
18+
// IAPClient is an interface to call validation API in Microsoft Store
19+
type IAPClient interface {
20+
Verify(context.Context, string, string) (IAPResponse, error)
21+
}
22+
23+
// Client implements IAPClient
24+
type Client struct {
25+
TenantID string
26+
ClientID string
27+
ClientSecret string
28+
httpCli *http.Client
29+
}
30+
31+
// New creates a client object
32+
func New(tenantId, clientId, secret string) *Client {
33+
client := &Client{
34+
TenantID: tenantId,
35+
ClientID: clientId,
36+
ClientSecret: secret,
37+
httpCli: &http.Client{
38+
Timeout: 10 * time.Second,
39+
},
40+
}
41+
42+
return client
43+
}
44+
45+
// Verify sends receipts and gets validation result
46+
func (c *Client) Verify(ctx context.Context, receipt IAPRequest) (IAPResponse, error) {
47+
resp := IAPResponse{}
48+
token, err := c.getAzureADToken(ctx, c.TenantID, c.ClientID, c.ClientSecret, resource)
49+
if err != nil {
50+
return resp, err
51+
}
52+
53+
return c.query(ctx, token, receipt)
54+
}
55+
56+
// getAzureADToken obtains an Azure AD access token using client credentials flow
57+
func (c *Client) getAzureADToken(ctx context.Context, tenantID, clientID, clientSecret, resource string) (string, error) {
58+
tokenURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", tenantID)
59+
60+
data := url.Values{}
61+
data.Set("grant_type", "client_credentials")
62+
data.Set("client_id", clientID)
63+
data.Set("client_secret", clientSecret)
64+
data.Set("resource", resource)
65+
66+
req, err := http.NewRequest("POST", tokenURL, bytes.NewBufferString(data.Encode()))
67+
if err != nil {
68+
return "", err
69+
}
70+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
71+
req.WithContext(ctx)
72+
73+
resp, err := c.httpCli.Do(req)
74+
if err != nil {
75+
return "", err
76+
}
77+
defer resp.Body.Close()
78+
79+
if resp.StatusCode != http.StatusOK {
80+
bodyBytes, _ := io.ReadAll(resp.Body)
81+
return "", fmt.Errorf("failed to obtain token: %s", string(bodyBytes))
82+
}
83+
84+
var tokenResponse struct {
85+
AccessToken string `json:"access_token"`
86+
}
87+
err = json.NewDecoder(resp.Body).Decode(&tokenResponse)
88+
if err != nil {
89+
return "", err
90+
}
91+
return tokenResponse.AccessToken, nil
92+
}
93+
94+
// query sends a query to Microsoft Store API
95+
func (c *Client) query(ctx context.Context, accessToken string, receiptData IAPRequest) (IAPResponse, error) {
96+
queryURL := "https://collections.mp.microsoft.com/v6.0/collections/query"
97+
result := IAPResponse{}
98+
99+
requestBody, err := json.Marshal(receiptData)
100+
if err != nil {
101+
return result, err
102+
}
103+
104+
req, err := http.NewRequest("POST", queryURL, bytes.NewBuffer(requestBody))
105+
if err != nil {
106+
return result, err
107+
}
108+
req.Header.Set("Content-Type", "application/json")
109+
req.Header.Set("Authorization", "Bearer "+accessToken)
110+
req.WithContext(ctx)
111+
112+
res, err := c.httpCli.Do(req)
113+
if err != nil {
114+
return result, err
115+
}
116+
defer res.Body.Close()
117+
118+
if res.StatusCode != http.StatusOK {
119+
bodyBytes, _ := io.ReadAll(res.Body)
120+
return result, fmt.Errorf("validation failed: %s", string(bodyBytes))
121+
}
122+
123+
err = json.NewDecoder(res.Body).Decode(&result)
124+
return result, err
125+
}

0 commit comments

Comments
 (0)