Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .travis.yml

This file was deleted.

31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
v2.0.0
-------------------------
- **BREAKING**: remove `Transaction#url` (its return type was inconsistent — a
String after `.create`, an Array after `getTransactionInfo`). Use the new
`Transaction#sign_url` (always a String) and `Transaction#signer_id` instead.
- **BREAKING**: rename `TransactionSigner#birtdate=` to `#birthdate=` (typo fix).
- **BREAKING**: raise required Ruby version to `>= 3.0`.
- **BREAKING**: every internal `raise "string"` now raises a typed
`Universign::Error` subclass (`UnknownOption`, `InvalidSignatureField`,
`SignatureFieldsMustBeAnArray`, `CheckBoxTextsMustBeAnArray`) instead of a
bare `RuntimeError`.
- Implement `Transaction#signers`, returning the signers progression as
`Universign::SignerInfos` beans (previously raised `NotImplementedYet`).
- Fix `Signer#first_name`/`#last_name` readers, which were looking up keys the
setters never wrote.
- Make `Document.from_data`/`Signer.from_data` thread-safe (no more shared
class-level state).
- Simplify `Safeguard`: drop the unused callback mechanism, name the `73020`
fault code, and match by fault code before falling back to fault strings.
- Internal cleanup: `params` is now the single source of truth for documents
and signers, `.travis.yml` removed.
- `Transaction` no longer fetches the transaction info eagerly in the
constructor: `getTransactionInfo` is now performed lazily on first access to
`data` (or any attribute relying on it). `.create` therefore no longer makes
a redundant second API call, and `sign_url`/`signer_id` are available without
any extra request.
- Test suite no longer depends on VCR/WebMock/dotenv: the XML-RPC client is
stubbed directly.
- Document the ability to send a document by URL (`Universign::Document.new(url:)`)
so Universign downloads it itself, instead of uploading base64 content.

v1.6.0
-------------------------
- Added the possibility to add required checkboxes to `Universign::Document`.
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
source 'https://rubygems.org'
source "https://rubygems.org"

# Specify your gem's dependencies in universign.gemspec
gemspec
85 changes: 78 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# RubyUniversign

RubyUniversign is a Ruby gem for interacting with [Universign](https://www.universign.com/) electronic signature API.
RubyUniversign is a Ruby gem for interacting with the [Universign](https://www.universign.com/) electronic signature API.

It ease requests to Universign API, documents uploads and following signature state.
It eases requests to the Universign API: uploading documents, requesting signatures and following their state.

This gem is **not** officialy made by Universign, but was originally created by [CapSens](https://capsens.eu/) for internal usage.
This gem is **not** officially made by Universign, but was originally created by [CapSens](https://capsens.eu/) for internal usage.

This gem currently integrate electronic signature service, but not other Universign services (timestamping and server stamp).
It currently integrates the electronic signature service only, not the other Universign services (timestamping and server stamp).

## Requirements

- Ruby `>= 3.0`
- An Universign account (login / password / endpoint)

## Installation

Expand Down Expand Up @@ -73,9 +78,12 @@ transaction = Universign::Transaction.create(
options: { profile: 'default', final_doc_sent: true }
)

transaction.url
transaction.sign_url
# => "https://sign.test.universign.eu/fr/signature/?id=f052e35e-a792-4440-bb67-6b5c3f17aa30"

transaction.signer_id
# => "f052e35e-a792-4440-bb67-6b5c3f17aa30"

transaction.transaction_id
# => "9696179e-a43d-4803-beeb-9e5c02fd159b"

Expand All @@ -85,6 +93,22 @@ transaction = Universign::Transaction.new('9696179e-a43d-4803-beeb-9e5c02fd159b'
transaction.signed?
```

When you rebuild a transaction from its id with `Universign::Transaction.new(id)`,
the transaction info is fetched **lazily**: no API call is made until you read an
attribute that needs it (`status`, `signed?`, `signers`, `documents`, …). Reading
several attributes only triggers a single `getTransactionInfo` call, which is then
memoized.

```ruby
transaction = Universign::Transaction.new('9696179e-...')

transaction.status # => "completed"
transaction.signed? # => true
transaction.signers # => [#<Universign::SignerInfos>, ...]
transaction.signers.first.status # => "signed"
transaction.documents # => [#<Universign::Document>, ...] (signed PDFs)
```

The gem also supports the updated way of creating multiple fields per document:

- Multiple signatures
Expand Down Expand Up @@ -178,17 +202,64 @@ Default options are:
For more informations on theses options, see Universign's official documentation

Once your transaction is created:
* `url` is where you must redirect your users for them to sign
* `sign_url` is where you must redirect your users for them to sign (always a String, available without any extra API call right after `.create`)
* `signer_id` is the signer id parsed from the sign URL
* `transaction_id` is the id you must save to retrieve it later. You can request up-to-date informations from Universign with `Universign::Transaction.new(transaction_id)`.
* `signers` returns the signers progression as `Universign::SignerInfos` beans
* `signed?` returns a boolean that tells you if the transaction is signed, or not !

## Error handling

Every error raised by the gem inherits from `Universign::Error`, so you can rescue
them all at once or handle them individually:

```ruby
begin
Universign::Transaction.create(documents: documents, signers: signers)
rescue Universign::NotEnoughTokens
# your Universign account ran out of tokens
rescue Universign::DocumentURLInvalid => e
# Universign could not download a document by URL
e.url # => the offending URL
rescue Universign::Error => e
# any other Universign error
end
```

The most common ones are:

| Exception | Raised when |
|------------------------------------------|--------------------------------------------------------|
| `Universign::InvalidCredentials` | login / password / endpoint are wrong |
| `Universign::NotEnoughTokens` | the account has no signature token left |
| `Universign::DocumentURLInvalid` | a document URL cannot be downloaded (`#url` exposes it) |
| `Universign::UnknownDocument` | the transaction or custom id is unknown |
| `Universign::DocumentNotSigned` | the document is not signed yet |
| `Universign::ErrorWhenSigningPDF` | Universign failed to sign the PDF |
| `Universign::UnknownOption` | an unknown option was passed to `.create` |
| `Universign::InvalidSignatureField` | a signature field is not a `Universign::SignatureField`|

## Universign documentation

As of September 25th 2018, all official Universign documentation can be found at https://help.universign.com/hc/fr/sections/360000148149-Guides-Universign.

## Upgrading from 1.x to 2.0

2.0 is a breaking release. The highlights:

- `Transaction#url` is removed. Use `Transaction#sign_url` (always a String) and
`Transaction#signer_id` instead.
- `Transaction.new(id)` no longer hits the API in the constructor — the info is
loaded lazily on first access.
- `TransactionSigner#birtdate=` is renamed to `#birthdate=`.
- Internal `raise "string"` calls are now typed `Universign::Error` subclasses.
- Minimum Ruby version is now `3.0`.

See the [CHANGELOG](CHANGELOG.md) for the full list.

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` (or `bundle exec rake spec`) to run the tests. The suite hits no network — the XML-RPC client is stubbed — and enforces 100% line and branch coverage via SimpleCov (report in `coverage/`). You can also run `bin/console` for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run `bundle exec rake install`.

Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require "bundler/gem_tasks"
require 'rspec/core/rake_task'
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)
8 changes: 4 additions & 4 deletions bin/console
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ require "universign"
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require 'dotenv'
require "dotenv"

Dotenv.load

Universign.configure do |config|
config.endpoint = ENV['UNIVERSIGN_ENDPOINT']
config.login = ENV['UNIVERSIGN_LOGIN']
config.password = ENV['UNIVERSIGN_PASSWORD']
config.endpoint = ENV["UNIVERSIGN_ENDPOINT"]
config.login = ENV["UNIVERSIGN_LOGIN"]
config.password = ENV["UNIVERSIGN_PASSWORD"]
end

require "irb"
Expand Down
28 changes: 14 additions & 14 deletions lib/universign.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
require 'active_support/core_ext/hash/indifferent_access'
require "active_support/core_ext/hash/indifferent_access"

require "universign/version"
require 'xmlrpc/client'
require 'universign/service/document'
require 'universign/service/transaction'
require 'universign/safeguard'
require 'universign/signer'
require 'universign/transaction'
require 'universign/signature_field'
require 'universign/signer_infos'
require 'universign/transaction_signer'
require 'universign/error'
require 'universign/client'
require 'universign/document'
require 'universign/configuration'
require "xmlrpc/client"
require "universign/service/document"
require "universign/service/transaction"
require "universign/safeguard"
require "universign/signer"
require "universign/transaction"
require "universign/signature_field"
require "universign/signer_infos"
require "universign/transaction_signer"
require "universign/error"
require "universign/client"
require "universign/document"
require "universign/configuration"

module Universign
end
21 changes: 15 additions & 6 deletions lib/universign/client.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
require 'singleton'

module Universign
class Client
attr_reader :client

# Convenience: build a fresh client and forward a call to it. A new
# client (and connection) is used per call, which keeps the wrapper
# thread-safe — XMLRPC::Client is not safe to share across threads.
def self.call(...)
new.call(...)
end

def initialize
@client = XMLRPC::Client.new2(
@client = XMLRPC::Client.new2(
Universign.configuration.endpoint,
Universign.configuration.proxy,
Universign.configuration.timeout
Expand All @@ -14,12 +19,16 @@ def initialize
@client.password = Universign.configuration.password
end

def method_missing(method, *args, &block)
def method_missing(method, ...)
if @client.respond_to?(method)
@client.send(method, *args, &block)
@client.send(method, ...)
else
super(method, *args, &block)
super
end
end

def respond_to_missing?(method, include_private = false)
@client.respond_to?(method, include_private) || super
end
end
end
52 changes: 22 additions & 30 deletions lib/universign/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ module Universign
class Document
include Universign::Safeguard

attr_reader :name, :file_content, :file_url
attr_accessor :params

# Create a new Universign::Document
Expand All @@ -25,96 +24,89 @@ def initialize(options = {})
# @param [Hash] data
# @return [Universign::Document]
def self.from_data(data)
@params = data

document = Universign::Document.new
document.params.merge!(@params)
document = new
document.params.merge!(data)
document
end

# The raw content of the PDF document
#
# @return [Array<Byte>]
def content
@content ||= params['content']
@content ||= params["content"]
end

def content=(data)
@content = data
params[:content] = XMLRPC::Base64.new(data)
params[:content] = XMLRPC::Base64.new(data)
end

# The URL to download the PDF document
#
# @return [String]
def url
@url ||= params['url']
params["url"]
end

def url=(data)
@url = data
params['url'] = data
params["url"] = data
end

# The type of this document
#
# @return [String]
def document_type
@document_type ||= params['documentType']
params["documentType"]
end

# The file name of this document
#
# @return [String]
def name
@name ||= params['name']
params["name"]
end

def name=(data)
@name = data
params['name'] = data
params["name"] = data
end

def signature_fields=(data)
if !data.is_a?(Array)
raise 'SignatureFieldsMustBeAnArray'
end
raise Universign::SignatureFieldsMustBeAnArray unless data.is_a?(Array)

@signature_fields = data
params['signatureFields'] = data.map do |d|
raise 'BadSignatureFieldType' unless d.instance_of?(SignatureField)
params["signatureFields"] = data.map do |d|
unless d.instance_of?(SignatureField)
raise Universign::InvalidSignatureField
end

d.params
end
end

def check_box_texts
@check_box_texts ||= params["checkBoxTexts"]
params["checkBoxTexts"]
end

def check_box_texts=(data)
if !data.is_a?(Array)
raise "CheckBoxTextsMustBeAnArray"
end
raise Universign::CheckBoxTextsMustBeAnArray unless data.is_a?(Array)

@check_box_texts = data
params["checkBoxTexts"] = data
end

# The meta data of the PDF document
# The meta data of the PDF document. Kept verbatim (the ivar) rather
# than read back from params, which would otherwise stringify the
# caller's symbol keys through HashWithIndifferentAccess.
#
# @return [Hash]
def meta_data
@meta_data ||= params['metaData']
@meta_data ||= params["metaData"]
end

def meta_data=(data)
if !data.is_a?(Hash)
raise MetaDataMustBeAHash
end
raise Universign::MetaDataMustBeAHash unless data.is_a?(Hash)

@meta_data = data
params['metaData'] = data
params["metaData"] = data
end
end
end
Loading