github.com/plutov/paypal/v4@v4.7.1/client.go (about) 1 package paypal 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "net/http/httputil" 12 "time" 13 ) 14 15 // NewClient returns new Client struct 16 // APIBase is a base API URL, for testing you can use paypal.APIBaseSandBox 17 func NewClient(clientID string, secret string, APIBase string) (*Client, error) { 18 if clientID == "" || secret == "" || APIBase == "" { 19 return nil, errors.New("ClientID, Secret and APIBase are required to create a Client") 20 } 21 22 return &Client{ 23 Client: &http.Client{}, 24 ClientID: clientID, 25 Secret: secret, 26 APIBase: APIBase, 27 }, nil 28 } 29 30 // GetAccessToken returns struct of TokenResponse 31 // No need to call SetAccessToken to apply new access token for current Client 32 // Endpoint: POST /v1/oauth2/token 33 func (c *Client) GetAccessToken(ctx context.Context) (*TokenResponse, error) { 34 buf := bytes.NewBuffer([]byte("grant_type=client_credentials")) 35 req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/oauth2/token"), buf) 36 if err != nil { 37 return &TokenResponse{}, err 38 } 39 40 req.Header.Set("Content-type", "application/x-www-form-urlencoded") 41 42 response := &TokenResponse{} 43 err = c.SendWithBasicAuth(req, response) 44 45 // Set Token fur current Client 46 if response.Token != "" { 47 c.Token = response 48 c.tokenExpiresAt = time.Now().Add(time.Duration(response.ExpiresIn) * time.Second) 49 } 50 51 return response, err 52 } 53 54 // SetHTTPClient sets *http.Client to current client 55 func (c *Client) SetHTTPClient(client *http.Client) { 56 c.Client = client 57 } 58 59 // SetAccessToken sets saved token to current client 60 func (c *Client) SetAccessToken(token string) { 61 c.Token = &TokenResponse{ 62 Token: token, 63 } 64 c.tokenExpiresAt = time.Time{} 65 } 66 67 // SetLog will set/change the output destination. 68 // If log file is set paypal will log all requests and responses to this Writer 69 func (c *Client) SetLog(log io.Writer) { 70 c.Log = log 71 } 72 73 // SetReturnRepresentation enables verbose response 74 // Verbose response: https://developer.paypal.com/docs/api/orders/v2/#orders-authorize-header-parameters 75 func (c *Client) SetReturnRepresentation() { 76 c.returnRepresentation = true 77 } 78 79 // Send makes a request to the API, the response body will be 80 // unmarshalled into v, or if v is an io.Writer, the response will 81 // be written to it without decoding 82 func (c *Client) Send(req *http.Request, v interface{}) error { 83 var ( 84 err error 85 resp *http.Response 86 data []byte 87 ) 88 89 // Set default headers 90 req.Header.Set("Accept", "application/json") 91 req.Header.Set("Accept-Language", "en_US") 92 93 // Default values for headers 94 if req.Header.Get("Content-type") == "" { 95 req.Header.Set("Content-type", "application/json") 96 } 97 if c.returnRepresentation { 98 req.Header.Set("Prefer", "return=representation") 99 } 100 101 resp, err = c.Client.Do(req) 102 c.log(req, resp) 103 104 if err != nil { 105 return err 106 } 107 defer func(Body io.ReadCloser) error { 108 return Body.Close() 109 }(resp.Body) 110 111 if resp.StatusCode < 200 || resp.StatusCode > 299 { 112 errResp := &ErrorResponse{Response: resp} 113 data, err = io.ReadAll(resp.Body) 114 115 if err == nil && len(data) > 0 { 116 err := json.Unmarshal(data, errResp) 117 if err != nil { 118 return err 119 } 120 } 121 122 return errResp 123 } 124 if v == nil { 125 return nil 126 } 127 128 if w, ok := v.(io.Writer); ok { 129 _, err := io.Copy(w, resp.Body) 130 return err 131 } 132 133 return json.NewDecoder(resp.Body).Decode(v) 134 } 135 136 // SendWithAuth makes a request to the API and apply OAuth2 header automatically. 137 // If the access token soon to be expired or already expired, it will try to get a new one before 138 // making the main request 139 // client.Token will be updated when changed 140 func (c *Client) SendWithAuth(req *http.Request, v interface{}) error { 141 // c.Lock() 142 c.mu.Lock() 143 // Note: Here we do not want to `defer c.Unlock()` because we need `c.Send(...)` 144 // to happen outside of the locked section. 145 146 if c.Token == nil || (!c.tokenExpiresAt.IsZero() && time.Until(c.tokenExpiresAt) < RequestNewTokenBeforeExpiresIn) { 147 // c.Token will be updated in GetAccessToken call 148 if _, err := c.GetAccessToken(req.Context()); err != nil { 149 // c.Unlock() 150 c.mu.Unlock() 151 return err 152 } 153 } 154 155 req.Header.Set("Authorization", "Bearer "+c.Token.Token) 156 // Unlock the client mutex before sending the request, this allows multiple requests 157 // to be in progress at the same time. 158 // c.Unlock() 159 c.mu.Unlock() 160 return c.Send(req, v) 161 } 162 163 // SendWithBasicAuth makes a request to the API using clientID:secret basic auth 164 func (c *Client) SendWithBasicAuth(req *http.Request, v interface{}) error { 165 req.SetBasicAuth(c.ClientID, c.Secret) 166 167 return c.Send(req, v) 168 } 169 170 // NewRequest constructs a request 171 // Convert payload to a JSON 172 func (c *Client) NewRequest(ctx context.Context, method, url string, payload interface{}) (*http.Request, error) { 173 var buf io.Reader 174 if payload != nil { 175 b, err := json.Marshal(&payload) 176 if err != nil { 177 return nil, err 178 } 179 buf = bytes.NewBuffer(b) 180 } 181 return http.NewRequestWithContext(ctx, method, url, buf) 182 } 183 184 // log will dump request and response to the log file 185 func (c *Client) log(r *http.Request, resp *http.Response) { 186 if c.Log != nil { 187 var ( 188 reqDump string 189 respDump []byte 190 ) 191 192 if r != nil { 193 reqDump = fmt.Sprintf("%s %s. Data: %s", r.Method, r.URL.String(), r.Form.Encode()) 194 } 195 if resp != nil { 196 respDump, _ = httputil.DumpResponse(resp, true) 197 } 198 199 c.Log.Write([]byte(fmt.Sprintf("Request: %s\nResponse: %s\n", reqDump, string(respDump)))) 200 } 201 }